- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
314 lines
9.0 KiB
JavaScript
314 lines
9.0 KiB
JavaScript
/**
|
|
* Web Push Manager Module
|
|
*
|
|
* Handles Web Push Notification subscriptions and management.
|
|
*/
|
|
|
|
export class WebPushManager {
|
|
constructor(options = {}) {
|
|
this.apiBase = options.apiBase || '/api/push';
|
|
this.serviceWorkerUrl = options.serviceWorkerUrl || '/js/sw-push.js';
|
|
this.vapidPublicKey = options.vapidPublicKey || null;
|
|
this.onSubscriptionChange = options.onSubscriptionChange || null;
|
|
}
|
|
|
|
/**
|
|
* Initialize Web Push Manager
|
|
*/
|
|
async init() {
|
|
if (!('serviceWorker' in navigator)) {
|
|
throw new Error('Service Workers are not supported in this browser');
|
|
}
|
|
|
|
if (!('PushManager' in window)) {
|
|
throw new Error('Push API is not supported in this browser');
|
|
}
|
|
|
|
// Register Service Worker
|
|
await this.registerServiceWorker();
|
|
|
|
// Get VAPID public key from server if not provided
|
|
if (!this.vapidPublicKey) {
|
|
await this.fetchVapidPublicKey();
|
|
}
|
|
|
|
// Check current subscription status
|
|
const subscription = await this.getSubscription();
|
|
|
|
if (this.onSubscriptionChange) {
|
|
this.onSubscriptionChange(subscription);
|
|
}
|
|
|
|
return subscription !== null;
|
|
}
|
|
|
|
/**
|
|
* Register Service Worker
|
|
*/
|
|
async registerServiceWorker() {
|
|
try {
|
|
const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl);
|
|
console.log('[WebPush] Service Worker registered', registration);
|
|
return registration;
|
|
} catch (error) {
|
|
console.error('[WebPush] Service Worker registration failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch VAPID public key from server
|
|
*/
|
|
async fetchVapidPublicKey() {
|
|
try {
|
|
const response = await fetch(`${this.apiBase}/vapid-key`);
|
|
const data = await response.json();
|
|
|
|
if (!data.public_key) {
|
|
throw new Error('VAPID public key not available');
|
|
}
|
|
|
|
this.vapidPublicKey = data.public_key;
|
|
console.log('[WebPush] VAPID public key fetched');
|
|
} catch (error) {
|
|
console.error('[WebPush] Failed to fetch VAPID public key', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request notification permission
|
|
*/
|
|
async requestPermission() {
|
|
const permission = await Notification.requestPermission();
|
|
console.log('[WebPush] Permission:', permission);
|
|
|
|
if (permission !== 'granted') {
|
|
throw new Error('Notification permission denied');
|
|
}
|
|
|
|
return permission;
|
|
}
|
|
|
|
/**
|
|
* Subscribe to push notifications
|
|
*/
|
|
async subscribe() {
|
|
try {
|
|
// Request permission first
|
|
await this.requestPermission();
|
|
|
|
// Get Service Worker registration
|
|
const registration = await navigator.serviceWorker.ready;
|
|
|
|
// Check if already subscribed
|
|
let subscription = await registration.pushManager.getSubscription();
|
|
|
|
if (subscription) {
|
|
console.log('[WebPush] Already subscribed', subscription);
|
|
} else {
|
|
// Subscribe
|
|
subscription = await registration.pushManager.subscribe({
|
|
userVisibleOnly: true,
|
|
applicationServerKey: this.urlBase64ToUint8Array(this.vapidPublicKey)
|
|
});
|
|
|
|
console.log('[WebPush] Subscribed', subscription);
|
|
}
|
|
|
|
// Send subscription to server
|
|
await this.sendSubscriptionToServer(subscription);
|
|
|
|
if (this.onSubscriptionChange) {
|
|
this.onSubscriptionChange(subscription);
|
|
}
|
|
|
|
return subscription;
|
|
} catch (error) {
|
|
console.error('[WebPush] Subscription failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unsubscribe from push notifications
|
|
*/
|
|
async unsubscribe() {
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const subscription = await registration.pushManager.getSubscription();
|
|
|
|
if (!subscription) {
|
|
console.log('[WebPush] Not subscribed');
|
|
return false;
|
|
}
|
|
|
|
// Unsubscribe from browser
|
|
const success = await subscription.unsubscribe();
|
|
|
|
if (success) {
|
|
console.log('[WebPush] Unsubscribed from browser');
|
|
|
|
// Remove from server
|
|
await this.removeSubscriptionFromServer(subscription);
|
|
|
|
if (this.onSubscriptionChange) {
|
|
this.onSubscriptionChange(null);
|
|
}
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
console.error('[WebPush] Unsubscribe failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current subscription
|
|
*/
|
|
async getSubscription() {
|
|
try {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
return await registration.pushManager.getSubscription();
|
|
} catch (error) {
|
|
console.error('[WebPush] Failed to get subscription', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if subscribed
|
|
*/
|
|
async isSubscribed() {
|
|
const subscription = await this.getSubscription();
|
|
return subscription !== null;
|
|
}
|
|
|
|
/**
|
|
* Send test notification
|
|
*/
|
|
async sendTestNotification(title, body) {
|
|
try {
|
|
const subscription = await this.getSubscription();
|
|
|
|
if (!subscription) {
|
|
throw new Error('Not subscribed');
|
|
}
|
|
|
|
const response = await fetch(`${this.apiBase}/test`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
endpoint: subscription.endpoint,
|
|
title: title || 'Test Notification',
|
|
body: body || 'This is a test notification!'
|
|
})
|
|
});
|
|
|
|
const result = await response.json();
|
|
console.log('[WebPush] Test notification sent', result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[WebPush] Test notification failed', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send subscription to server
|
|
*/
|
|
async sendSubscriptionToServer(subscription) {
|
|
try {
|
|
const subscriptionJson = subscription.toJSON();
|
|
|
|
const response = await fetch(`${this.apiBase}/subscribe`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(subscriptionJson)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server returned ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[WebPush] Subscription sent to server', result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[WebPush] Failed to send subscription to server', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove subscription from server
|
|
*/
|
|
async removeSubscriptionFromServer(subscription) {
|
|
try {
|
|
const response = await fetch(`${this.apiBase}/unsubscribe`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
endpoint: subscription.endpoint
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Server returned ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
console.log('[WebPush] Subscription removed from server', result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
console.error('[WebPush] Failed to remove subscription from server', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert Base64 URL to Uint8Array (for 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;
|
|
}
|
|
|
|
/**
|
|
* Get notification permission status
|
|
*/
|
|
getPermissionStatus() {
|
|
return Notification.permission;
|
|
}
|
|
|
|
/**
|
|
* Check if browser supports Web Push
|
|
*/
|
|
static isSupported() {
|
|
return ('serviceWorker' in navigator) && ('PushManager' in window);
|
|
}
|
|
}
|
|
|
|
export default WebPushManager;
|