135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
// 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
|
|
}`,
|
|
);
|
|
});
|