Add web push notification system for user engagement and updates

Implement a web push notification system, including service worker integration, user subscription management, and an admin interface for sending broadcast messages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f585829f-898b-492f-82a5-11f4a76c87fb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/ICRgny1
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic
2026-03-07 15:16:12 +00:00
parent 8e7dc999f0
commit f6478a7663
11 changed files with 423 additions and 1 deletions
+79
View File
@@ -13,6 +13,7 @@ import multer from "multer";
import path from "path";
import fs from "fs";
import https from "https";
import webpush from "web-push";
const apiCache = new Map<string, { data: any; timestamp: number }>();
function getCached<T>(key: string, ttlMs: number): T | null {
@@ -495,6 +496,84 @@ export async function registerRoutes(
}
});
const vapidPublic = process.env.VAPID_PUBLIC_KEY || "";
const vapidPrivate = process.env.VAPID_PRIVATE_KEY || "";
if (vapidPublic && vapidPrivate) {
webpush.setVapidDetails("mailto:office@folx.tv", vapidPublic, vapidPrivate);
}
app.get("/api/push/vapid-key", (_req, res) => {
res.json({ publicKey: vapidPublic });
});
app.post("/api/push/subscribe", async (req, res) => {
try {
const { endpoint, keys } = req.body;
if (!endpoint || !keys?.p256dh || !keys?.auth) {
return res.status(400).json({ message: "Ungültige Subscription-Daten." });
}
await storage.savePushSubscription({ endpoint, p256dh: keys.p256dh, auth: keys.auth });
res.json({ ok: true });
} catch (err: any) {
res.status(500).json({ message: err.message });
}
});
app.post("/api/push/unsubscribe", async (req, res) => {
try {
const { endpoint } = req.body;
if (!endpoint) return res.status(400).json({ message: "Endpoint fehlt." });
await storage.deletePushSubscription(endpoint);
res.json({ ok: true });
} catch (err: any) {
res.status(500).json({ message: err.message });
}
});
app.get("/api/push/count", async (_req, res) => {
try {
const count = await storage.getPushSubscriptionCount();
res.json({ count });
} catch {
res.json({ count: 0 });
}
});
app.post("/api/admin/push/send", async (req, res) => {
try {
const { title, body, url } = req.body;
if (!title || !body) return res.status(400).json({ message: "Titel und Nachricht sind erforderlich." });
if (typeof title !== "string" || title.length > 200) return res.status(400).json({ message: "Titel ungültig." });
if (typeof body !== "string" || body.length > 500) return res.status(400).json({ message: "Nachricht ungültig." });
let safeUrl = "/";
if (url && typeof url === "string" && url.startsWith("/") && !url.startsWith("//")) {
safeUrl = url;
}
const subs = await storage.getAllPushSubscriptions();
const payload = JSON.stringify({ title, body, url: safeUrl });
let sent = 0;
let failed = 0;
for (const sub of subs) {
try {
await webpush.sendNotification(
{ endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } },
payload
);
sent++;
} catch (err: any) {
if (err.statusCode === 410 || err.statusCode === 404) {
await storage.deletePushSubscription(sub.endpoint);
}
failed++;
}
}
console.log(`[push] Sent to ${sent}, failed ${failed} of ${subs.length} subscribers`);
res.json({ ok: true, sent, failed, total: subs.length });
} catch (err: any) {
res.status(500).json({ message: err.message });
}
});
app.get("/api/gallery/artists", (_req, res) => {
try {
const aPath = path.join(process.cwd(), "server/gallery-artist-overrides.json");