From 97b8260b33802d1c01a097d06d3fb33f77412424 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 15 May 2026 12:43:33 -0500 Subject: [PATCH] Fix VAPID deployment config --- docker-compose.onprem.yml | 3 +++ docker-compose.yml | 5 ++++- server/server.js | 26 +++++++++++++++++++------- src/App.vue | 11 +++-------- src/services/notifications.js | 9 ++++++--- 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/docker-compose.onprem.yml b/docker-compose.onprem.yml index 0d12687..cfe3955 100644 --- a/docker-compose.onprem.yml +++ b/docker-compose.onprem.yml @@ -21,6 +21,9 @@ services: BTCPAY_STORE_ID: ${BTCPAY_STORE_ID:-} BTCPAY_API_KEY: ${BTCPAY_API_KEY:-} BTCPAY_WEBHOOK_SECRET: ${BTCPAY_WEBHOOK_SECRET:-} + VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} + VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} + VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@l484.com} volumes: - l484-onprem-data:/app/server/data diff --git a/docker-compose.yml b/docker-compose.yml index b850357..773448d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: NODE_ENV: production HOST: 0.0.0.0 PORT: 2354 - APP_MODE: public + APP_MODE: all MEMBERSHIP_ENCRYPTION_KEY: ${MEMBERSHIP_ENCRYPTION_KEY:?Set a unique 64-character hex key} ACCESS_HMAC_KEY: ${ACCESS_HMAC_KEY:?Set a unique access HMAC key} ACCESS_CONTROLLER_TOKEN: ${ACCESS_CONTROLLER_TOKEN:-} @@ -21,6 +21,9 @@ services: BTCPAY_STORE_ID: ${BTCPAY_STORE_ID:-} BTCPAY_API_KEY: ${BTCPAY_API_KEY:-} BTCPAY_WEBHOOK_SECRET: ${BTCPAY_WEBHOOK_SECRET:-} + VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} + VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} + VAPID_SUBJECT: ${VAPID_SUBJECT:-mailto:admin@l484.com} volumes: - l484-public-data:/app/server/data diff --git a/server/server.js b/server/server.js index 1bf82b5..f699fb7 100644 --- a/server/server.js +++ b/server/server.js @@ -26,9 +26,20 @@ const btcpayStoreId = process.env.BTCPAY_STORE_ID || '' const btcpayWebhookSecret = process.env.BTCPAY_WEBHOOK_SECRET || '' const homeAssistantUnlockWebhookUrl = process.env.HOME_ASSISTANT_UNLOCK_WEBHOOK_URL || '' const homeAssistantUnlockTimeoutMs = Number(process.env.HOME_ASSISTANT_UNLOCK_TIMEOUT_MS || 2500) -const vapidPublicKey = process.env.VAPID_PUBLIC_KEY || '' -const vapidPrivateKey = process.env.VAPID_PRIVATE_KEY || '' -const vapidSubject = process.env.VAPID_SUBJECT || 'mailto:admin@l484.com' +const cleanEnvValue = (value) => String(value || '').trim().replace(/^["']|["']$/g, '') +const vapidPublicKey = cleanEnvValue(process.env.VAPID_PUBLIC_KEY) +const vapidPrivateKey = cleanEnvValue(process.env.VAPID_PRIVATE_KEY) +const vapidSubject = cleanEnvValue(process.env.VAPID_SUBJECT) || 'mailto:admin@l484.com' +const isValidVapidPublicKey = (value) => { + try { + const clean = cleanEnvValue(value) + const padding = '='.repeat((4 - (clean.length % 4)) % 4) + const key = Buffer.from((clean + padding).replace(/-/g, '+').replace(/_/g, '/'), 'base64') + return key.length === 65 && key[0] === 4 + } catch { + return false + } +} const normalizeAdminPubkey = (value) => { const raw = String(value || '').trim().toLowerCase() if (/^[0-9a-f]{64}$/.test(raw)) return raw @@ -68,7 +79,7 @@ const rateBuckets = new Map() const publicApiEnabled = () => appMode !== 'admin' const adminApiEnabled = () => appMode !== 'public' -if (vapidPublicKey && vapidPrivateKey) { +if (isValidVapidPublicKey(vapidPublicKey) && vapidPrivateKey) { webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey) } @@ -385,15 +396,16 @@ const publicAdminRequests = () => state.adminRequests.map((request) => ({ })) const notificationStats = () => ({ - configured: Boolean(vapidPublicKey && vapidPrivateKey), + configured: Boolean(isValidVapidPublicKey(vapidPublicKey) && vapidPrivateKey), subscriberCount: state.notificationSubscriptions.length, publicKeyConfigured: Boolean(vapidPublicKey), + publicKeyValid: isValidVapidPublicKey(vapidPublicKey), privateKeyConfigured: Boolean(vapidPrivateKey), subject: vapidSubject, }) const sendPushNotification = async ({ title, message, url = '/edit', icon = '/images/small-logo.svg', tag = 'l484-update' }) => { - if (!vapidPublicKey || !vapidPrivateKey) { + if (!isValidVapidPublicKey(vapidPublicKey) || !vapidPrivateKey) { return { success: false, reason: 'vapid_not_configured', sent: 0, failed: 0 } } const payload = JSON.stringify({ @@ -920,7 +932,7 @@ const handleApi = async (req, res) => { } if (req.method === 'GET' && url.pathname === '/api/notifications/vapid-public-key') { - return json(res, 200, { success: true, publicKey: vapidPublicKey, configured: Boolean(vapidPublicKey && vapidPrivateKey) }) + return json(res, 200, { success: true, publicKey: isValidVapidPublicKey(vapidPublicKey) ? vapidPublicKey : '', configured: Boolean(isValidVapidPublicKey(vapidPublicKey) && vapidPrivateKey) }) } if (req.method === 'GET' && url.pathname === '/api/notifications/test-vapid') { diff --git a/src/App.vue b/src/App.vue index 17f9c74..ed3a5bb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -3601,14 +3601,9 @@ watch(mobileMenuOpen, (open) => { - + diff --git a/src/services/notifications.js b/src/services/notifications.js index 29494a8..5c57c63 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -3,10 +3,13 @@ import { ref } from 'vue' const permission = ref(typeof Notification === 'undefined' ? 'unsupported' : Notification.permission) const urlBase64ToUint8Array = (base64String) => { - const padding = '='.repeat((4 - (base64String.length % 4)) % 4) - const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/') + const clean = String(base64String || '').trim().replace(/^["']|["']$/g, '') + const padding = '='.repeat((4 - (clean.length % 4)) % 4) + const base64 = (clean + padding).replace(/-/g, '+').replace(/_/g, '/') const rawData = window.atob(base64) - return new Uint8Array([...rawData].map((char) => char.charCodeAt(0))) + const key = new Uint8Array([...rawData].map((char) => char.charCodeAt(0))) + if (key.length !== 65 || key[0] !== 4) throw new Error('VAPID public key is not valid.') + return key } export const notificationPermission = permission