Files
michaelschiemer/resources/js/modules/webpush/WebPushManager.js
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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;