Fix VAPID deployment config
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
11
src/App.vue
11
src/App.vue
@@ -3601,14 +3601,9 @@ watch(mobileMenuOpen, (open) => {
|
||||
<button v-else-if="signupStep < 5" class="primary-action" type="button" @click="nextStep">
|
||||
Continue
|
||||
</button>
|
||||
<template v-else-if="signupStep === 5">
|
||||
<button class="secondary-action" type="button" :disabled="isSignupNotificationLoading" @click="createMembership">
|
||||
Continue without alerts
|
||||
</button>
|
||||
<button class="primary-action" type="button" :disabled="isSignupNotificationLoading" @click="enableSignupNotificationsAndCreate">
|
||||
{{ isSignupNotificationLoading ? 'Enabling...' : 'Enable & create card' }}
|
||||
</button>
|
||||
</template>
|
||||
<button v-else-if="signupStep === 5" class="primary-action" type="button" :disabled="isSignupNotificationLoading" @click="enableSignupNotificationsAndCreate">
|
||||
{{ isSignupNotificationLoading ? 'Enabling...' : 'Enable & create card' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user