feat: Implement secure VAPID key generation for push notifications

- Replace random key generation with the web-push library for generating cryptographically secure VAPID keys.
- Update console output to guide users on adding keys to their environment configuration.
- Enhance error handling for VAPID key generation issues.
- Add web-push dependency to package.json and package-lock.json for proper functionality.
This commit is contained in:
padreug 2025-07-07 00:38:32 +02:00
parent cc6ba2612d
commit 8a9ffc5918
5 changed files with 169 additions and 36 deletions

View file

@ -1,36 +1,28 @@
#!/usr/bin/env node
// Simple VAPID key generator for testing push notifications
// In production, you'd want to use proper cryptographic libraries
// Proper VAPID key generator using web-push library
import webpush from 'web-push'
console.log('🔑 VAPID Key Generator for Push Notifications')
console.log('')
// Generate a random string for testing
function generateTestKey(length = 64) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
// Generate test VAPID keys
const publicKey = generateTestKey(87) // Base64 URL-safe, typical length
const privateKey = generateTestKey(43) // Base64 URL-safe, typical length
console.log('📋 Add these to your .env file:')
console.log('')
console.log(`VITE_VAPID_PUBLIC_KEY=${publicKey}`)
console.log(`VITE_PUSH_NOTIFICATIONS_ENABLED=true`)
console.log('')
console.log('⚠️ IMPORTANT: These are test keys for development only!')
console.log(' For production, generate proper VAPID keys using:')
console.log(' - web-push library: npx web-push generate-vapid-keys')
console.log(' - online tool: https://vapidkeys.com/')
console.log('')
console.log('🔐 Private key (keep secure, for backend only):')
console.log(`VAPID_PRIVATE_KEY=${privateKey}`)
console.log('')
console.log('✅ Once added, restart your dev server to apply the changes.')
try {
// Generate proper VAPID keys using web-push
const vapidKeys = webpush.generateVAPIDKeys()
console.log('📋 Add these to your .env file:')
console.log('')
console.log(`VITE_VAPID_PUBLIC_KEY=${vapidKeys.publicKey}`)
console.log(`VITE_PUSH_NOTIFICATIONS_ENABLED=true`)
console.log('')
console.log('🔐 Private key (keep secure, for backend only):')
console.log(`VAPID_PRIVATE_KEY=${vapidKeys.privateKey}`)
console.log('')
console.log('✅ Once added, restart your dev server to apply the changes.')
console.log('')
console.log(' These are cryptographically secure VAPID keys suitable for production.')
} catch (error) {
console.error('❌ Error generating VAPID keys:', error)
process.exit(1)
}

127
package-lock.json generated
View file

@ -60,7 +60,8 @@
"vite-plugin-image-optimizer": "^1.1.7",
"vite-plugin-inspect": "^0.8.3",
"vite-plugin-pwa": "^0.21.1",
"vue-tsc": "^2.2.0"
"vue-tsc": "^2.2.0",
"web-push": "^3.6.7"
}
},
"node_modules/@ampproject/remapping": {
@ -5544,6 +5545,16 @@
"node": ">=0.4.0"
}
},
"node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
@ -5709,6 +5720,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@ -5851,6 +5875,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/bn.js": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
"dev": true,
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@ -5958,6 +5989,13 @@
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -7018,6 +7056,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ejs": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
@ -8669,6 +8717,16 @@
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/http_ece": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz",
"integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
@ -8690,6 +8748,20 @@
"node": ">=10.19.0"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
@ -9534,6 +9606,29 @@
"node": ">=8"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"dev": true,
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -10450,6 +10545,13 @@
"mini-svg-data-uri": "cli.js"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"dev": true,
"license": "ISC"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@ -12304,8 +12406,7 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true,
"license": "MIT",
"optional": true
"license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
@ -14029,6 +14130,26 @@
"defaults": "^1.0.3"
}
},
"node_modules/web-push": {
"version": "3.6.7",
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
"integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
"asn1.js": "^5.3.0",
"http_ece": "1.2.0",
"https-proxy-agent": "^7.0.0",
"jws": "^4.0.0",
"minimist": "^1.2.5"
},
"bin": {
"web-push": "src/cli.js"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/web-vitals": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz",

View file

@ -69,7 +69,8 @@
"vite-plugin-image-optimizer": "^1.1.7",
"vite-plugin-inspect": "^0.8.3",
"vite-plugin-pwa": "^0.21.1",
"vue-tsc": "^2.2.0"
"vue-tsc": "^2.2.0",
"web-push": "^3.6.7"
},
"build": {
"appId": "com.yourdomain.aio-shadcn-vite",

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed } from 'vue'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'

View file

@ -101,11 +101,15 @@ export class PushNotificationService {
// Check if already subscribed
const existingSubscription = await registration.pushManager.getSubscription()
if (existingSubscription) {
console.log('Using existing push subscription')
return this.subscriptionToData(existingSubscription)
}
// Create new subscription
console.log('Creating new push subscription with VAPID key:', config.push.vapidPublicKey.substring(0, 20) + '...')
const applicationServerKey = this.urlBase64ToUint8Array(config.push.vapidPublicKey)
console.log('VAPID key converted to Uint8Array, length:', applicationServerKey.length)
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
@ -114,11 +118,26 @@ export class PushNotificationService {
const subscriptionData = this.subscriptionToData(subscription)
console.log('Push subscription created:', subscriptionData)
console.log('Push subscription created successfully:', {
endpoint: subscriptionData.endpoint,
hasKeys: !!subscriptionData.keys.p256dh && !!subscriptionData.keys.auth
})
return subscriptionData
} catch (error) {
console.error('Failed to subscribe to push notifications:', error)
// Provide more specific error information
if (error instanceof DOMException) {
if (error.name === 'InvalidStateError') {
console.error('VAPID key format issue detected. Please check your VAPID public key.')
} else if (error.name === 'NotAllowedError') {
console.error('Push notifications blocked by user or browser policy.')
} else if (error.name === 'NotSupportedError') {
console.error('Push notifications not supported by this browser.')
}
}
throw error
}
}