chore: push notification test
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# Install web-push library
|
||||
npm install web-push
|
||||
# or
|
||||
pnpm add web-push
|
||||
```
|
||||
|
||||
### 4. Run Database Migration
|
||||
|
||||
```bash
|
||||
# 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`:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
|
||||
```javascript
|
||||
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:
|
||||
|
||||
```html
|
||||
<!-- 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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```bash
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
Reference in New Issue
Block a user