// src/index.ts import express, { Express } from 'express'; import dotenv from 'dotenv'; import { json } from 'body-parser'; import webpush from 'web-push'; import crypto from 'crypto'; import sqlite3 from 'sqlite3'; import cors from 'cors'; dotenv.config(); interface Subscription { id: string; endpoint: string; keys: string; } const app: Express = express(); const port = process.env.PORT || 3000; let secret = process.env.SECRET; let vapidKeys = { publicKey: process.env.VAPID_PUBLIC_KEY, privateKey: process.env.VAPID_PRIVATE_KEY, }; if (!vapidKeys.publicKey || !vapidKeys.privateKey || secret === undefined) { console.log( 'SECRET, VAPID_PUBLIC_KEY, or VAPID_PRIVATE_KEY is not defined. Please run `npx run generate-keys` to generate VAPID keys, server secret, and store them in a `.env` file at the root of the project.', ); process.exit(1); } webpush.setVapidDetails( `mailto:${process.env.VAPID_EMAIL}`, vapidKeys.publicKey, vapidKeys.privateKey, ); const db = new sqlite3.Database('subscriptions.db'); db.serialize(() => { db.run(` CREATE TABLE IF NOT EXISTS subscriptions ( id TEXT, endpoint TEXT NOT NULL, keys TEXT NOT NULL, PRIMARY KEY (id, endpoint) ) `); }); function createHash(publicKey: string, secret: string): string { const publicKeyBuffer = Buffer.from(publicKey, 'base64'); const hash = crypto.createHmac('sha256', secret); hash.update(publicKeyBuffer); return hash.digest('hex'); } app.use(json()); app.use( cors({ origin: process.env.NODE_ENV === 'production' ? process.env.PROD_ORIGIN : '*', }), ); app.post('/api/subscribe', async (req, res) => { let { subscription, publicKey } = req.body; const id = await createHash(publicKey, secret!); await db.run( 'INSERT OR REPLACE INTO subscriptions (id, endpoint, keys) VALUES (?, ?, ?)', id, subscription.endpoint, JSON.stringify(subscription.keys), ); res.status(201).json({}); }); app.get('/api/public-key', (req, res) => { res.status(200).json({ publicKey: vapidKeys.publicKey }); }); app.post('/api/send-notification', async (req, res) => { let { publicKey, url } = req.body; const id = createHash(publicKey, secret!); // get the subscription from the database db.all( 'SELECT * FROM subscriptions WHERE id = ?', id, (err, subscriptions: Subscription[]) => { if (err) { console.error(err); return res .status(500) .send({ error: 'Failed to retrieve subscriptions' }); } if (subscriptions) { const payload = JSON.stringify({ body: 'You have a new encrypted message! Click here to view it.', data: { url: url, }, }); Promise.all( subscriptions.map((subscription) => { const keys = JSON.parse(subscription.keys); return webpush.sendNotification( { endpoint: subscription.endpoint, keys }, payload, ); }), ) .then(() => res.status(200).send({ success: true })) .catch((error) => { console.error(error); res.status(500).send({ error: 'Failed to send notification' }); }); } else { res.status(404).send({ error: 'No subscriptions found for this id' }); } }, ); }); app.listen(port, () => { console.log( `[server]: Server is running at ${ process.env.NODE_ENV ? process.env.PROD_ORIGIN : process.env.DEV_ORIGIN }`, ); });