diff --git a/package-lock.json b/package-lock.json index f212166..01bd540 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2991,9 +2991,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001520", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz", - "integrity": "sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA==", + "version": "1.0.30001521", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz", + "integrity": "sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==", "dev": true, "funding": [ { @@ -3596,9 +3596,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.491", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.491.tgz", - "integrity": "sha512-ZzPqGKghdVzlQJ+qpfE+r6EB321zed7e5JsvHIlMM4zPFF8okXUkF5Of7h7F3l3cltPL0rG7YVmlp5Qro7RQLA==", + "version": "1.4.492", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.492.tgz", + "integrity": "sha512-36K9b/6skMVwAIEsC7GiQ8I8N3soCALVSHqWHzNDtGemAcI9Xu8hP02cywWM0A794rTHm0b0zHPeLJHtgFVamQ==", "dev": true }, "node_modules/elliptic": { @@ -5279,9 +5279,9 @@ } }, "node_modules/jackspeak": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.3.tgz", - "integrity": "sha512-pF0kfjmg8DJLxDrizHoCZGUFz4P4czQ3HyfW4BU0ffebYkzAVlBywp5zaxW/TM+r0sGbmrQdi8EQQVTJFxnGsQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.0.tgz", + "integrity": "sha512-uKmsITSsF4rUWQHzqaRUuyAir3fZfW3f202Ee34lz/gZCi970CPZwyQXLGNgWJvvZbvFyzeyGq0+4fcG/mBKZg==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -7297,9 +7297,9 @@ } }, "node_modules/prettier": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz", - "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.2.tgz", + "integrity": "sha512-o2YR9qtniXvwEZlOKbveKfDQVyqxbEIWn48Z8m3ZJjBjcCmUy3xZGIv+7AkaeuaTr6yPXJjwv07ZWlsWbEy1rQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" diff --git a/src/VapidKeys.ts b/src/VapidKeys.ts new file mode 100644 index 0000000..60d5af2 --- /dev/null +++ b/src/VapidKeys.ts @@ -0,0 +1,14 @@ +// Subscription.ts +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity() +export class VapidKeys { + @PrimaryGeneratedColumn() + id: number; + + @Column() + publicKey: string; + + @Column() + privateKey: string; +} diff --git a/src/db.ts b/src/db.ts index 7bb908e..d3139d4 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,10 +1,13 @@ import { DataSource, Repository } from "typeorm"; import {Subscription} from './Subscription.js' +import { VapidKeys } from './VapidKeys.js'; export class DBService { private dataSource: DataSource; + private vapidSource: DataSource; private subscriptionRepository: Repository; + private vapidRepository: Repository; constructor() { this.dataSource = new DataSource({ @@ -12,7 +15,15 @@ export class DBService { database: "subscriptions", entities: [Subscription], }); - this.dataSource.getRepository(Subscription) + this.subscriptionRepository = this.dataSource.getRepository(Subscription) + + this.vapidSource = new DataSource({ + type: "sqlite", + database: "vapidKeys", + entities: [VapidKeys], + }); + + this.vapidRepository = this.vapidSource.getRepository(VapidKeys); } async saveSubscription(endpoint: string, keys_p256dh: string, keys_auth: string) { @@ -26,4 +37,15 @@ export class DBService { async getSubscriptions(): Promise { return this.subscriptionRepository.find(); } + + async getVapidKeys(): Promise { + return this.vapidRepository.find(); + } + + async saveVapidKeys(publicKey: string, privateKey: string) { + const vapidkeys = new VapidKeys(); + vapidkeys.privateKey = privateKey; + vapidkeys.publicKey = publicKey; + return this.vapidRepository.save(vapidkeys); + } } diff --git a/src/main.ts b/src/main.ts index 7c34886..987d0e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { Subscription, SubscriptionService } from './subscriptionService.js'; +import { SubscriptionService } from './subscriptionService.js'; import express, { Express, Request, Response } from 'express'; import { Worker } from 'worker_threads'; import { fileURLToPath } from 'url'; @@ -7,6 +7,14 @@ import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +export interface Subscription { + endpoint: string; + keys: { + p256dh: string; + auth: string; + }; +} + class Server { private app: Express; private port: number; diff --git a/src/notificationService.ts b/src/notificationService.ts index e1d48f9..d06e3a7 100644 --- a/src/notificationService.ts +++ b/src/notificationService.ts @@ -1,8 +1,10 @@ -import { SubscriptionService, Subscription } from './subscriptionService.js'; -import { VapidService, VapidKeys } from './vapidService.js'; +import { SubscriptionService } from './subscriptionService.js'; +import { VapidService } from './vapidService.js'; +import { VapidKeys } from './VapidKeys.js'; import * as https from 'https'; import * as http_ece from 'http_ece'; import crypto from 'crypto'; +import { Subscription } from "./Subscription.js" export interface Message { title: string; @@ -14,7 +16,6 @@ export class NotificationService { private subscriptionService: SubscriptionService; private vapidService: VapidService; - private vapidKeys: VapidKeys; constructor() { this.subscriptionService = new SubscriptionService(); @@ -27,7 +28,6 @@ export class NotificationService { async broadcast(message: Message): Promise { const subscriptions = await this.subscriptionService.fetchSubscriptions(); - this.vapidKeys = await this.vapidService.getVapidKeys(); for (const subscription of subscriptions) { await this.pushToEndpoint(subscription, message); @@ -35,14 +35,13 @@ export class NotificationService { } async sendNotification(subscription: Subscription, message: Message) { - this.vapidKeys = await this.vapidService.getVapidKeys(); await this.pushToEndpoint(subscription, message); } private async pushToEndpoint(subscription: Subscription, message: Message): Promise { const payload = JSON.stringify(message); - const encrypted = this.encrypt(subscription.keys.p256dh, subscription.keys.auth, payload); + const encrypted = this.encrypt(subscription.keys_p256dh, subscription.keys_auth, payload); const endpoint = subscription.endpoint; const vapidHeaders = await this.vapidService.createVapidAuthHeader(endpoint, 12 * 60 * 60, 'mailto:example@example.com'); @@ -86,12 +85,12 @@ export class NotificationService { 'auth': auth }; + const vapidKeys: VapidKeys = this.vapidService.getVapidKeys()[0]; return http_ece.encrypt(payload, { 'salt': this.generateSalt(), - 'dh': this.vapidKeys.publicKey, + 'dh': vapidKeys.publicKey, 'keyid': 'p256dh', 'contentEncoding': 'aes128gcm' }); } - } diff --git a/src/subscriptionService.ts b/src/subscriptionService.ts index 04c1bb8..5755aa1 100644 --- a/src/subscriptionService.ts +++ b/src/subscriptionService.ts @@ -1,6 +1,7 @@ import { DBService } from './db.js'; +import { Subscription } from './Subscription.js'; -export interface Subscription { +export interface SubscriptionData { endpoint: string; keys: { p256dh: string; @@ -15,7 +16,7 @@ export class SubscriptionService { this.dbService = new DBService(); } - async addSubscription(subscription: Subscription): Promise { + async addSubscription(subscription: SubscriptionData): Promise { await this.dbService.saveSubscription( subscription.endpoint, subscription.keys.p256dh, diff --git a/src/vapidService.ts b/src/vapidService.ts index 81a1a24..5fcfcd9 100644 --- a/src/vapidService.ts +++ b/src/vapidService.ts @@ -1,25 +1,24 @@ -import { Database } from 'sqlite3'; + import jwt from 'jsonwebtoken'; import crypto from 'crypto'; +import { DBService } from './db.js'; +import { VapidKeys } from './VapidKeys.js'; -export interface VapidKeys { +export interface VapidKeyData { publicKey: string; privateKey: string; } export class VapidService { - private db: Database; + private dbService: DBService; constructor() { - this.db = new Database('vapidKeys.db'); - this.initializeDatabase(); - } - - private initializeDatabase(): void { - this.db.run(`CREATE TABLE IF NOT EXISTS vapid (id INTEGER PRIMARY KEY, publicKey TEXT, privateKey TEXT)`); + this.dbService = new DBService(); + const keys = this.generateVAPIDKeys(); + this.addVapidKeys(keys); } - private generateVAPIDKeys(): VapidKeys { + private generateVAPIDKeys(): VapidKeyData { const ecdh = crypto.createECDH('prime256v1'); ecdh.generateKeys(); @@ -29,26 +28,19 @@ export class VapidService { }; } - async getVapidKeys(): Promise { - return new Promise((resolve, reject) => { - this.db.get('SELECT publicKey, privateKey FROM vapid WHERE id = ?', [1], (err, row: VapidKeys) => { - if (err) reject(err); + async addVapidKeys(vapidkeys: VapidKeyData) { + const keys = await this.getVapidKeys(); + if (keys.length == 0) { + this.dbService.saveVapidKeys(vapidkeys.publicKey, vapidkeys.privateKey); + } + } - if (row) { - resolve({ publicKey: row.publicKey, privateKey: row.privateKey }); - } else { - const keys = this.generateVAPIDKeys(); - this.db.run('INSERT INTO vapid (publicKey, privateKey) VALUES (?, ?)', [keys.publicKey, keys.privateKey], (err) => { - if (err) reject(err); - resolve(keys); - }); - } - }); - }); + async getVapidKeys(): Promise { + return this.dbService.getVapidKeys(); } async createVapidAuthHeader(endpoint: string, expiration: number, subject: string): Promise<{ 'Authorization': string, 'Crypto-Key': string }> { - const { publicKey, privateKey } = await this.getVapidKeys(); + const { publicKey, privateKey } = await this.getVapidKeys()[0]; const jwtInfo = { aud: new URL(endpoint).origin, diff --git a/src/worker.ts b/src/worker.ts index 5091ebb..fada985 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -8,8 +8,9 @@ class WorkerThread { constructor(interval: number) { this.interval = interval; + this.notificationService = new NotificationService(); - this.message.title = "Check the app."; + this.message = { "title": "Check the app." }; this.startPeriodicTask(); }