diff --git a/changedetectionio/notification/apprise_plugin/browser_notification_helpers.py b/changedetectionio/notification/apprise_plugin/browser_notification_helpers.py index d21b8507e..2ff7da057 100644 --- a/changedetectionio/notification/apprise_plugin/browser_notification_helpers.py +++ b/changedetectionio/notification/apprise_plugin/browser_notification_helpers.py @@ -11,32 +11,44 @@ from loguru import logger def convert_pem_private_key_for_pywebpush(private_key): """ - Convert PEM private key to the raw bytes format that pywebpush expects + Convert PEM private key to the format that pywebpush expects Args: private_key: PEM private key string or already converted key Returns: - Private key in the format pywebpush expects + Private key in the format pywebpush expects (PEM string for pywebpush) """ - if not isinstance(private_key, str) or not private_key.startswith('-----BEGIN'): + # pywebpush expects the PEM string directly + if not isinstance(private_key, str): + return private_key + + # If it doesn't look like PEM, return as-is + if not private_key.startswith('-----BEGIN'): return private_key try: from cryptography.hazmat.primitives import serialization - private_key_bytes = private_key.encode() + from cryptography.hazmat.primitives.asymmetric import ec + + # Validate the key by loading it + private_key_bytes = private_key.encode('utf-8') private_key_obj = serialization.load_pem_private_key(private_key_bytes, password=None) - # Get raw private key bytes for pywebpush - private_key_raw = private_key_obj.private_bytes( - encoding=serialization.Encoding.Raw, - format=serialization.PrivateFormat.Raw, - encryption_algorithm=serialization.NoEncryption() - ) - return private_key_raw + # Verify it's an ECDSA key (required for VAPID) + if not isinstance(private_key_obj, ec.EllipticCurvePrivateKey): + logger.error("Private key is not an ECDSA key - VAPID requires ECDSA") + return private_key + + # Ensure the key has the right curve (P-256 for VAPID) + if private_key_obj.curve.name != 'secp256r1': + logger.warning(f"Private key uses curve {private_key_obj.curve.name}, VAPID recommends secp256r1 (P-256)") + + # Return the original PEM - pywebpush handles PEM format correctly + return private_key except Exception as e: - logger.warning(f"Failed to convert private key format, using as-is: {e}") + logger.warning(f"Failed to validate private key format: {e}") return private_key diff --git a/changedetectionio/static/js/browser-notifications.js b/changedetectionio/static/js/browser-notifications.js index d34e55861..f0101bfbe 100644 --- a/changedetectionio/static/js/browser-notifications.js +++ b/changedetectionio/static/js/browser-notifications.js @@ -159,6 +159,9 @@ class BrowserNotifications { return; } + // First, try to clear any existing subscription with different keys + await this.clearExistingSubscription(); + // Create push subscription const subscription = await this.serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, @@ -196,7 +199,13 @@ class BrowserNotifications { } catch (error) { console.error(`Failed to subscribe to keyword ${keyword}:`, error); - alert(`Failed to subscribe: ${error.message}`); + + // Show user-friendly error message + if (error.message.includes('different applicationServerKey')) { + this.showSubscriptionConflictDialog(keyword, error); + } else { + alert(`Failed to subscribe: ${error.message}`); + } } } @@ -319,6 +328,74 @@ class BrowserNotifications { return outputArray; } + async clearExistingSubscription() { + /** + * Clear any existing push subscription that might conflict with our VAPID keys + */ + try { + const existingSubscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); + + if (existingSubscription) { + console.log('Found existing subscription, unsubscribing...'); + await existingSubscription.unsubscribe(); + console.log('Successfully cleared existing subscription'); + } + } catch (error) { + console.warn('Failed to clear existing subscription:', error); + // Don't throw - this is just cleanup + } + } + + showSubscriptionConflictDialog(keyword, error) { + /** + * Show user-friendly dialog for subscription conflicts + */ + const message = `Browser notifications are already set up for a different changedetection.io instance or with different settings. + +To fix this: +1. Clear your existing subscription +2. Try subscribing again + +Would you like to automatically clear the old subscription and retry?`; + + if (confirm(message)) { + this.clearExistingSubscription().then(() => { + // Retry subscription after clearing + setTimeout(() => { + this.subscribeToKeyword(keyword); + }, 500); + }); + } else { + alert('To use browser notifications, please manually clear your browser notifications for this site in browser settings, then try again.'); + } + } + + async clearAllNotifications() { + /** + * Clear all browser notification subscriptions (admin function) + */ + try { + // Clear service worker subscription + const existingSubscription = await this.serviceWorkerRegistration.pushManager.getSubscription(); + if (existingSubscription) { + await existingSubscription.unsubscribe(); + } + + // Clear local storage + this.subscriptions.clear(); + + // Update UI + this.updateSubscriptionsList(); + + console.log('All notifications cleared'); + alert('All browser notifications have been cleared. You can now subscribe again.'); + + } catch (error) { + console.error('Failed to clear all notifications:', error); + alert('Failed to clear notifications. Please manually clear them in browser settings.'); + } + } + async handleAutoSubscription() { // Handle auto-subscription for keywords detected from browser:// URLs try { diff --git a/changedetectionio/templates/_common_fields.html b/changedetectionio/templates/_common_fields.html index 1ae41c52c..2795fc142 100644 --- a/changedetectionio/templates/_common_fields.html +++ b/changedetectionio/templates/_common_fields.html @@ -41,6 +41,7 @@