Files
maternal-app/maternal-web/lib/push-notifications.ts
Andrei 9b31d56c1d
Some checks failed
ParentFlow CI/CD Pipeline / Backend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Frontend Tests (push) Has been cancelled
ParentFlow CI/CD Pipeline / Security Scanning (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-app/maternal-app-backend dockerfile:Dockerfile.production name:backend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Build Docker Images (map[context:maternal-web dockerfile:Dockerfile.production name:frontend]) (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Development (push) Has been cancelled
ParentFlow CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
Backend CI/CD Pipeline / Lint and Test Backend (push) Has been cancelled
Backend CI/CD Pipeline / E2E Tests Backend (push) Has been cancelled
Backend CI/CD Pipeline / Build Backend Application (push) Has been cancelled
Backend CI/CD Pipeline / Performance Testing (push) Has been cancelled
feat: Add persistent global notification settings for admin panel
Implement database-backed notification settings that persist across restarts:

Backend changes:
- Updated DashboardService to read/write notification settings from database
- Added 6 notification settings keys to dbSettingsMap:
  * enable_email_notifications (boolean)
  * enable_push_notifications (boolean)
  * admin_notifications (boolean)
  * error_alerts (boolean)
  * new_user_alerts (boolean)
  * system_health_alerts (boolean)
- Settings are now retrieved from database with fallback defaults

Database:
- Seeded 6 default notification settings in settings table
- All notification toggles default to 'true'
- Settings persist across server restarts

Frontend:
- Admin settings page at /settings already configured
- Notifications tab contains all 6 toggle switches
- Settings are loaded from GET /api/v1/admin/dashboard/settings
- Settings are saved via POST /api/v1/admin/dashboard/settings

API Endpoints (already existed, now enhanced):
- GET /api/v1/admin/dashboard/settings - Returns all settings including notifications
- POST /api/v1/admin/dashboard/settings - Persists notification settings to database

Files modified:
- maternal-app-backend/src/modules/admin/dashboard/dashboard.service.ts

Benefits:
 Global notification settings are now persistent
 Admin can control email/push notifications globally
 Admin can configure alert preferences
 Settings survive server restarts and deployments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 23:09:13 +00:00

325 lines
8.5 KiB
TypeScript

// Push Notifications Utility for ParentFlow Web App
// Handles Web Push subscription and management
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://maternal-api.noru1.ro';
export interface PushSubscriptionData {
endpoint: string;
keys: {
p256dh: string;
auth: string;
};
}
/**
* Convert VAPID public key from base64 to Uint8Array
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
/**
* Check if push notifications are supported
*/
export function isPushNotificationSupported(): boolean {
if (typeof window === 'undefined') return false;
return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;
}
/**
* Get current notification permission status
*/
export function getNotificationPermission(): NotificationPermission {
if (typeof window === 'undefined' || !('Notification' in window)) {
return 'default';
}
return Notification.permission;
}
/**
* Request notification permission
*/
export async function requestNotificationPermission(): Promise<NotificationPermission> {
if (!isPushNotificationSupported()) {
throw new Error('Push notifications are not supported in this browser');
}
const permission = await Notification.requestPermission();
console.log('[Push] Permission result:', permission);
return permission;
}
/**
* Get or fetch the VAPID public key
*/
export async function getVapidPublicKey(token?: string): Promise<string> {
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}/api/v1/push/vapid-public-key`, {
method: 'GET',
headers,
});
if (!response.ok) {
throw new Error(`Failed to get VAPID key: ${response.statusText}`);
}
const data = await response.json();
return data.publicKey;
} catch (error) {
console.error('[Push] Error getting VAPID key:', error);
throw error;
}
}
/**
* Register service worker for push notifications
*/
export async function registerPushServiceWorker(): Promise<ServiceWorkerRegistration> {
if (!('serviceWorker' in navigator)) {
throw new Error('Service Worker not supported');
}
try {
// Check if already registered
const existingRegistration = await navigator.serviceWorker.getRegistration('/push-sw.js');
if (existingRegistration) {
console.log('[Push] Service Worker already registered');
return existingRegistration;
}
// Register new service worker
const registration = await navigator.serviceWorker.register('/push-sw.js', {
scope: '/',
});
console.log('[Push] Service Worker registered:', registration.scope);
// Wait for the service worker to be ready
await navigator.serviceWorker.ready;
return registration;
} catch (error) {
console.error('[Push] Service Worker registration failed:', error);
throw error;
}
}
/**
* Subscribe to push notifications
*/
export async function subscribeToPush(token: string): Promise<PushSubscription> {
if (!isPushNotificationSupported()) {
throw new Error('Push notifications are not supported');
}
// Request permission first
const permission = await requestNotificationPermission();
if (permission !== 'granted') {
throw new Error('Notification permission denied');
}
try {
// Register service worker
const registration = await registerPushServiceWorker();
// Get VAPID public key
const vapidPublicKey = await getVapidPublicKey(token);
const applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
// Subscribe to push
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey,
});
console.log('[Push] Subscribed to push notifications');
// Send subscription to backend
await savePushSubscription(subscription, token);
return subscription;
} catch (error) {
console.error('[Push] Error subscribing to push:', error);
throw error;
}
}
/**
* Save push subscription to backend
*/
export async function savePushSubscription(
subscription: PushSubscription,
token: string
): Promise<void> {
try {
const subscriptionData = subscription.toJSON();
const response = await fetch(`${API_BASE_URL}/api/v1/push/subscriptions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(subscriptionData),
});
if (!response.ok) {
throw new Error(`Failed to save subscription: ${response.statusText}`);
}
console.log('[Push] Subscription saved to backend');
} catch (error) {
console.error('[Push] Error saving subscription:', error);
throw error;
}
}
/**
* Get current push subscription
*/
export async function getPushSubscription(): Promise<PushSubscription | null> {
if (!isPushNotificationSupported()) {
return null;
}
try {
const registration = await navigator.serviceWorker.getRegistration('/push-sw.js');
if (!registration) {
return null;
}
const subscription = await registration.pushManager.getSubscription();
return subscription;
} catch (error) {
console.error('[Push] Error getting subscription:', error);
return null;
}
}
/**
* Unsubscribe from push notifications
*/
export async function unsubscribeFromPush(token: string): Promise<void> {
try {
const subscription = await getPushSubscription();
if (!subscription) {
console.log('[Push] No active subscription to unsubscribe');
return;
}
const endpoint = subscription.endpoint;
// Unsubscribe locally
await subscription.unsubscribe();
console.log('[Push] Unsubscribed locally');
// Remove from backend
await fetch(`${API_BASE_URL}/api/v1/push/subscriptions?endpoint=${encodeURIComponent(endpoint)}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log('[Push] Subscription removed from backend');
} catch (error) {
console.error('[Push] Error unsubscribing:', error);
throw error;
}
}
/**
* Check if user is subscribed to push notifications
*/
export async function isPushSubscribed(): Promise<boolean> {
const subscription = await getPushSubscription();
return subscription !== null;
}
/**
* Send a test push notification
*/
export async function sendTestPushNotification(token: string): Promise<void> {
try {
const response = await fetch(`${API_BASE_URL}/api/v1/push/test`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`Failed to send test notification: ${response.statusText}`);
}
console.log('[Push] Test notification sent');
} catch (error) {
console.error('[Push] Error sending test notification:', error);
throw error;
}
}
/**
* Get push notification statistics
*/
export async function getPushStatistics(token: string): Promise<any> {
try {
const response = await fetch(`${API_BASE_URL}/api/v1/push/statistics`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`Failed to get statistics: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('[Push] Error getting statistics:', error);
throw error;
}
}
/**
* Show a local test notification (no server needed)
*/
export async function showLocalTestNotification(): Promise<void> {
if (getNotificationPermission() !== 'granted') {
await requestNotificationPermission();
}
if (getNotificationPermission() === 'granted') {
const registration = await navigator.serviceWorker.getRegistration('/push-sw.js');
if (registration) {
await registration.showNotification('ParentFlow Test', {
body: 'This is a local test notification',
icon: '/icons/icon-192x192.png',
badge: '/icons/icon-72x72.png',
tag: 'test',
data: { url: '/' },
});
}
}
}