Files
dmail-api/WEB_PUSH_SETUP.md
T
2025-07-24 13:03:29 +03:30

17 KiB

Web Push Notifications Setup Guide

This guide explains how to implement and use Web Push notifications in the Dmail API. Web Push allows you to send notifications directly to users' browsers, even when your web app is not open.

Overview

The Web Push notification system sends real-time notifications to users' browsers when new emails arrive. It uses the standard Web Push API supported by all modern browsers and works with service workers.

Architecture

Email Received → EmailMonitoringService → NotificationQueue → NewEmailHandler → PushNotificationHandler → Browser Push Service → User's Browser

Setup Instructions

1. Generate VAPID Keys

VAPID (Voluntary Application Server Identification) keys are required for Web Push notifications:

# Generate VAPID keys using Node.js
node -e "
const webpush = require('web-push');
const vapidKeys = webpush.generateVAPIDKeys();
console.log('Public Key:', vapidKeys.publicKey);
console.log('Private Key:', vapidKeys.privateKey);
"

2. Configure Environment Variables

Add these environment variables to your .env file:

# Web Push Notifications Configuration
PUSH_NOTIFICATIONS_ENABLED=true
VAPID_PUBLIC_KEY=your_generated_public_key_here
VAPID_PRIVATE_KEY=your_generated_private_key_here
VAPID_SUBJECT=mailto:admin@yourdomain.com

# Optional: Push notification settings
PUSH_TTL=86400
PUSH_URGENCY=normal
PUSH_RETRY_ATTEMPTS=3
PUSH_RETRY_DELAY=5000

# Frontend URL for notification actions
FRONTEND_URL=https://mail.yourdomain.com

3. Install Dependencies

# Install web-push library
npm install web-push
# or
pnpm add web-push

4. Run Database Migration

# Apply the database migration for web push notification fields
psql -d your_database -f database/migrations/add_push_notifications_to_users.sql

5. Update App Module

Add the push notifications config to your app.module.ts:

import { pushNotificationsConfig } from "./configs/push-notifications.config";

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [
        // ... other configs
        pushNotificationsConfig,
      ],
    }),
    // ... other modules
  ],
})
export class AppModule {}

Frontend Implementation

1. Service Worker Setup

Create public/sw.js in your frontend project:

// Service Worker for handling push notifications
self.addEventListener("push", function (event) {
  if (event.data) {
    const data = event.data.json();

    const options = {
      body: data.body,
      icon: data.icon || "/assets/email-icon.png",
      badge: data.badge || "/assets/email-badge.png",
      tag: data.tag || "email-notification",
      requireInteraction: data.requireInteraction || false,
      actions: data.actions || [],
      data: data.data || {},
    };

    event.waitUntil(self.registration.showNotification(data.title, options));
  }
});

// Handle notification clicks
self.addEventListener("notificationclick", function (event) {
  event.notification.close();

  if (event.action === "view") {
    // Open the email in the app
    event.waitUntil(clients.openWindow(event.notification.data.url || "/"));
  } else if (event.action === "dismiss") {
    // Just close the notification
    return;
  } else {
    // Default action - open the app
    event.waitUntil(clients.openWindow(event.notification.data.url || "/"));
  }
});

2. Client-side JavaScript

Add this to your main JavaScript file:

class WebPushManager {
  constructor(apiBaseUrl, getAuthToken) {
    this.apiBaseUrl = apiBaseUrl;
    this.getAuthToken = getAuthToken;
    this.vapidPublicKey = null;
  }

  // Initialize and register service worker
  async init() {
    if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
      console.warn("Push messaging is not supported");
      return false;
    }

    try {
      // Register service worker
      const registration = await navigator.serviceWorker.register("/sw.js");
      console.log("Service Worker registered:", registration);

      // Get VAPID public key
      await this.getVAPIDKey();

      return true;
    } catch (error) {
      console.error("Service Worker registration failed:", error);
      return false;
    }
  }

  // Get VAPID public key from server
  async getVAPIDKey() {
    try {
      const response = await fetch(`${this.apiBaseUrl}/users/push-notifications/vapid-key`, {
        headers: {
          Authorization: `Bearer ${this.getAuthToken()}`,
        },
      });

      if (response.ok) {
        const data = await response.json();
        this.vapidPublicKey = data.vapidPublicKey;
        return this.vapidPublicKey;
      }
    } catch (error) {
      console.error("Failed to get VAPID key:", error);
    }
    return null;
  }

  // Request permission and subscribe to push notifications
  async subscribeToPush() {
    if (!this.vapidPublicKey) {
      await this.getVAPIDKey();
    }

    if (!this.vapidPublicKey) {
      throw new Error("VAPID public key not available");
    }

    // Request permission
    const permission = await Notification.requestPermission();
    if (permission !== "granted") {
      throw new Error("Push notification permission denied");
    }

    // Get service worker registration
    const registration = await navigator.serviceWorker.ready;

    try {
      // Subscribe to push notifications
      const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey),
      });

      // Send subscription to server
      await this.sendSubscriptionToServer(subscription);

      console.log("Successfully subscribed to push notifications");
      return subscription;
    } catch (error) {
      console.error("Failed to subscribe to push notifications:", error);
      throw error;
    }
  }

  // Send subscription to server
  async sendSubscriptionToServer(subscription) {
    const response = await fetch(`${this.apiBaseUrl}/users/push-subscriptions`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.getAuthToken()}`,
      },
      body: JSON.stringify({
        subscription: subscription.toJSON(),
        deviceType: "web",
        deviceName: navigator.userAgent.slice(0, 50),
      }),
    });

    if (!response.ok) {
      throw new Error("Failed to send subscription to server");
    }

    return response.json();
  }

  // Test push notification
  async testNotification() {
    const response = await fetch(`${this.apiBaseUrl}/users/push-notifications/test`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${this.getAuthToken()}`,
      },
      body: JSON.stringify({
        message: "Test notification from Dmail Web App",
      }),
    });

    if (!response.ok) {
      throw new Error("Failed to send test notification");
    }

    return response.json();
  }

  // Check if user is subscribed
  async isSubscribed() {
    if (!("serviceWorker" in navigator) || !("PushManager" in window)) {
      return false;
    }

    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.getSubscription();
    return !!subscription;
  }

  // Unsubscribe from push notifications
  async unsubscribe() {
    const registration = await navigator.serviceWorker.ready;
    const subscription = await registration.pushManager.getSubscription();

    if (subscription) {
      // Unsubscribe from browser
      await subscription.unsubscribe();

      // Remove from server
      await fetch(`${this.apiBaseUrl}/users/push-subscriptions`, {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
          Authorization: `Bearer ${this.getAuthToken()}`,
        },
        body: JSON.stringify({
          endpoint: subscription.endpoint,
        }),
      });

      console.log("Successfully unsubscribed from push notifications");
    }
  }

  // Utility function to convert VAPID key
  urlBase64ToUint8Array(base64String) {
    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;
  }
}

// Usage example
const pushManager = new WebPushManager("https://api.yourdomain.com", () => localStorage.getItem("authToken"));

// Initialize on page load
pushManager.init().then((success) => {
  if (success) {
    console.log("Web Push initialized successfully");
  }
});

// Export for use in your app
window.webPushManager = pushManager;

3. User Interface Integration

Add UI components to your app:

<!-- Notification permission UI -->
<div id="push-notification-ui" style="display: none;">
  <div class="notification-prompt">
    <h3>📬 Enable Email Notifications</h3>
    <p>Get instant notifications when new emails arrive, even when the app is closed.</p>
    <button id="enable-notifications" class="btn btn-primary">Enable Notifications</button>
    <button id="test-notification" class="btn btn-secondary" style="display: none;">Test Notification</button>
    <button id="disable-notifications" class="btn btn-danger" style="display: none;">Disable</button>
  </div>
</div>

<script>
  document.addEventListener("DOMContentLoaded", async () => {
    const ui = document.getElementById("push-notification-ui");
    const enableBtn = document.getElementById("enable-notifications");
    const testBtn = document.getElementById("test-notification");
    const disableBtn = document.getElementById("disable-notifications");

    // Show UI if push is supported
    if ("serviceWorker" in navigator && "PushManager" in window) {
      ui.style.display = "block";

      // Check current subscription status
      const isSubscribed = await window.webPushManager.isSubscribed();
      if (isSubscribed) {
        enableBtn.style.display = "none";
        testBtn.style.display = "inline-block";
        disableBtn.style.display = "inline-block";
      }

      // Enable notifications
      enableBtn.addEventListener("click", async () => {
        try {
          await window.webPushManager.subscribeToPush();
          enableBtn.style.display = "none";
          testBtn.style.display = "inline-block";
          disableBtn.style.display = "inline-block";
          alert("Push notifications enabled successfully!");
        } catch (error) {
          alert("Failed to enable notifications: " + error.message);
        }
      });

      // Test notification
      testBtn.addEventListener("click", async () => {
        try {
          await window.webPushManager.testNotification();
          alert("Test notification sent!");
        } catch (error) {
          alert("Failed to send test notification: " + error.message);
        }
      });

      // Disable notifications
      disableBtn.addEventListener("click", async () => {
        try {
          await window.webPushManager.unsubscribe();
          enableBtn.style.display = "inline-block";
          testBtn.style.display = "none";
          disableBtn.style.display = "none";
          alert("Push notifications disabled");
        } catch (error) {
          alert("Failed to disable notifications: " + error.message);
        }
      });
    }
  });
</script>

API Endpoints

Push Subscription Management

  • POST /users/push-subscriptions - Register a new push subscription
  • PATCH /users/push-subscriptions - Update push subscriptions
  • DELETE /users/push-subscriptions - Remove a push subscription

Push Notification Settings

  • PATCH /users/push-notifications/settings - Enable/disable push notifications
  • GET /users/push-notifications/info - Get push notification info and settings
  • POST /users/push-notifications/test - Send a test notification
  • GET /users/push-notifications/vapid-key - Get VAPID public key

Usage Examples

Register Push Subscription

curl -X POST https://api.yourdomain.com/users/push-subscriptions \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subscription": {
      "endpoint": "https://fcm.googleapis.com/fcm/send/...",
      "keys": {
        "p256dh": "BNbxxx...",
        "auth": "abc123..."
      }
    },
    "deviceType": "web",
    "deviceName": "Chrome on MacBook Pro"
  }'

Test Push Notification

curl -X POST https://api.yourdomain.com/users/push-notifications/test \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Test notification from Dmail API"
  }'

Get VAPID Public Key

curl -X GET https://api.yourdomain.com/users/push-notifications/vapid-key \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

How It Works

  1. Service Worker Registration: Your web app registers a service worker that can receive push messages
  2. User Permission: The user grants permission for notifications
  3. Subscription Creation: The browser creates a push subscription with a unique endpoint
  4. Server Registration: The subscription is sent to your API and stored in the database
  5. Email Detection: When new emails arrive, the EmailMonitoringService detects them
  6. Notification Processing: The NewEmailHandler triggers the PushNotificationHandler
  7. Push Message: The server sends a push message to the browser's push service
  8. Notification Display: The service worker receives the message and shows the notification

Browser Support

Web Push is supported by:

  • Chrome 50+
  • Firefox 44+
  • Safari 16.0+
  • Edge 17+
  • Opera 37+

Notification Flow

sequenceDiagram
    participant U as User's Browser
    participant SW as Service Worker
    participant API as Dmail API
    participant PS as Push Service
    participant E as Email System

    U->>SW: Register Service Worker
    U->>API: Request VAPID key
    API->>U: Return VAPID public key
    U->>U: Request notification permission
    U->>PS: Create push subscription
    PS->>U: Return subscription object
    U->>API: Register subscription
    API->>API: Store subscription

    E->>API: New email detected
    API->>PS: Send push message
    PS->>SW: Deliver push message
    SW->>U: Show notification

Security Considerations

  1. VAPID Keys: Keep your private VAPID key secure and never expose it to the client
  2. Subscription Validation: Always validate subscription objects before storing them
  3. Rate Limiting: Implement rate limiting to prevent notification spam
  4. Data Privacy: Be mindful of what data you include in push notifications
  5. User Consent: Always request explicit user permission before subscribing to push notifications

Troubleshooting

Common Issues

  1. Notifications not received:

    • Check if the user has granted notification permission
    • Verify the service worker is registered correctly
    • Ensure VAPID keys are properly configured
    • Check browser console for errors
  2. VAPID key errors:

    • Ensure VAPID keys are correctly generated and configured
    • Verify the VAPID subject is a valid mailto: or https: URL
  3. Service worker issues:

    • Check if the service worker file is accessible
    • Verify the service worker scope includes your web app
    • Ensure HTTPS is used (required for service workers)

Debug Commands

# Test VAPID key configuration
curl -X GET https://api.yourdomain.com/users/push-notifications/vapid-key \
  -H "Authorization: Bearer YOUR_TOKEN"

# Check user's notification settings
curl -X GET https://api.yourdomain.com/users/push-notifications/info \
  -H "Authorization: Bearer YOUR_TOKEN"

# Send test notification
curl -X POST https://api.yourdomain.com/users/push-notifications/test \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"message": "Test"}'

Monitoring and Analytics

  • Subscription Management: Track active subscriptions per user
  • Delivery Success: Monitor push notification delivery rates
  • User Engagement: Track notification click-through rates
  • Error Handling: Log and monitor push notification failures

Performance Considerations

  • Payload Size: Keep push notification payloads small (max 4KB)
  • Batch Processing: Process multiple subscriptions efficiently
  • TTL Settings: Configure appropriate Time-To-Live for notifications
  • Cleanup: Regularly remove invalid/expired subscriptions

Future Enhancements

  • Rich Notifications: Add images and action buttons
  • Notification Categories: Different notification types for different email categories
  • User Preferences: Granular control over notification timing and content
  • Analytics Dashboard: Web interface for monitoring push notification metrics
  • A/B Testing: Test different notification strategies and content

Support

For issues and questions:

  1. Check browser developer console for errors
  2. Verify service worker registration and subscription
  3. Test VAPID key configuration
  4. Review application logs for error messages