add push registration
This commit is contained in:
parent
8557a5d8bf
commit
2ec7855081
Binary file not shown.
9
assets/public/client/service-worker.js
Normal file
9
assets/public/client/service-worker.js
Normal file
@ -0,0 +1,9 @@
|
||||
self.addEventListener('push', function (event) {
|
||||
if (!event.data) return;
|
||||
const data = event.data.json();
|
||||
|
||||
const title = data.title || "Discord";
|
||||
const options = { body: data.body };
|
||||
|
||||
event.waitUntil(self.registration.showNotification(title, options));
|
||||
});
|
||||
Binary file not shown.
@ -18,7 +18,7 @@
|
||||
|
||||
import { Config, ConnectionConfig, ConnectionLoader, Email, JSONReplacer, WebAuthn, initDatabase, initEvent, registerRoutes } from "@spacebar/util";
|
||||
import { Authentication, CORS, ImageProxy, BodyParser, ErrorHandler, initRateLimits, initTranslation } from "./middlewares";
|
||||
import { Request, Response, Router } from "express";
|
||||
import { Router } from "express";
|
||||
import { Server, ServerOptions } from "lambert-server";
|
||||
import morgan from "morgan";
|
||||
import path from "path";
|
||||
@ -119,7 +119,7 @@ export class SpacebarServer extends Server {
|
||||
res.sendFile(path.join(ASSETS_FOLDER, "openapi.json"));
|
||||
});
|
||||
|
||||
// current well-known location (new commit 22 nov 2025 from spacebar)
|
||||
// current well-known location
|
||||
app.get("/.well-known/spacebar", (req, res) => {
|
||||
res.json({
|
||||
api: Config.get().api.endpointPublic,
|
||||
@ -152,6 +152,7 @@ export class SpacebarServer extends Server {
|
||||
encoding: [...(erlpackSupported ? ["etf"] : []), "json"],
|
||||
compression: ["zstd-stream", "zlib-stream", null],
|
||||
},
|
||||
publicVapidKey: Config.get().webPush.publicVapidKey,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -18,12 +18,61 @@
|
||||
|
||||
import { Router, Response, Request } from "express";
|
||||
import { route } from "@spacebar/api";
|
||||
import { DeviceNotificationSchema } from "@spacebar/schemas";
|
||||
import { Config, PushSubscription, Snowflake, vapidConfigured } from "@spacebar/util";
|
||||
import { HTTPError } from "lambert-server";
|
||||
import webpush from "web-push";
|
||||
|
||||
const router = Router({ mergeParams: true });
|
||||
|
||||
router.post("/", route({}), (req: Request, res: Response) => {
|
||||
// TODO:
|
||||
res.sendStatus(204);
|
||||
});
|
||||
router.post(
|
||||
"/",
|
||||
route({
|
||||
requestBody: "DeviceNotificationSchema",
|
||||
responses: {
|
||||
400: {
|
||||
body: "APIErrorResponse",
|
||||
},
|
||||
},
|
||||
}),
|
||||
async (req: Request, res: Response) => {
|
||||
const body = req.body as DeviceNotificationSchema;
|
||||
if (body.provider != "webpush") throw new HTTPError("Provider is not supported", 400);
|
||||
|
||||
if (!Config.get().webPush.enabled || !vapidConfigured) throw new HTTPError("WebPush notifications are not configured", 400);
|
||||
|
||||
const subscription = body.webpush_subscription;
|
||||
if (!subscription) throw new HTTPError("Subscription is missing or invalid", 400);
|
||||
|
||||
const endpoint = subscription.endpoint.trim();
|
||||
try {
|
||||
new URL(endpoint);
|
||||
} catch {
|
||||
throw new HTTPError("Endpoint is not a valid url", 400);
|
||||
}
|
||||
|
||||
await webpush.sendNotification(subscription, JSON.stringify({ body: "Swoosh. Notifications are a go!" })).catch(async (err) => {
|
||||
console.error("[WebPush] Failed to send test notification:", err.message);
|
||||
throw new HTTPError("Failed to send test notification", 400);
|
||||
});
|
||||
|
||||
await PushSubscription.upsert(
|
||||
{
|
||||
id: Snowflake.generate(),
|
||||
user_id: req.user_id ?? null,
|
||||
endpoint,
|
||||
expiration_time: subscription.expirationTime != null ? Number(subscription.expirationTime) : undefined,
|
||||
auth: subscription.keys.auth,
|
||||
p256dh: subscription.keys.p256dh,
|
||||
},
|
||||
{
|
||||
conflictPaths: ["endpoint"],
|
||||
skipUpdateIfNoValuesChanged: true,
|
||||
},
|
||||
);
|
||||
|
||||
res.sendStatus(204);
|
||||
},
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
34
src/schemas/uncategorised/DeviceNotificationSchema.ts
Normal file
34
src/schemas/uncategorised/DeviceNotificationSchema.ts
Normal file
@ -0,0 +1,34 @@
|
||||
export type PushProvider = "gcm" | "apns" | "apns_internal" | "apns_voip" | "apns_internal_voip" | "webpush";
|
||||
|
||||
export interface DeviceNotificationSchema {
|
||||
provider: PushProvider;
|
||||
token?: string;
|
||||
voip_provider?: PushProvider;
|
||||
voip_token?: string;
|
||||
webpush_subscription?: {
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 2048
|
||||
*/
|
||||
endpoint: string;
|
||||
/**
|
||||
* @minimum 0
|
||||
* @TJS-type integer
|
||||
*/
|
||||
expirationTime?: number;
|
||||
keys: {
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 256
|
||||
*/
|
||||
p256dh: string;
|
||||
/**
|
||||
* @minLength 1
|
||||
* @maxLength 256
|
||||
*/
|
||||
auth: string;
|
||||
};
|
||||
};
|
||||
bypass_server_throttling_supported?: boolean;
|
||||
bundle_id?: string;
|
||||
}
|
||||
@ -90,3 +90,4 @@ export * from "./WebhookCreateSchema";
|
||||
export * from "./WebhookExecuteSchema";
|
||||
export * from "./WebhookUpdateSchema";
|
||||
export * from "./WidgetModifySchema";
|
||||
export * from "./DeviceNotificationSchema";
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
export class WebPushConfiguration {
|
||||
enabled: boolean = false;
|
||||
email: string | null = null;
|
||||
subject: string | null = null;
|
||||
publicVapidKey: string | null = null;
|
||||
privateVapidKey: string | null = null;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export class PushSubscription extends BaseClass {
|
||||
@ManyToOne(() => User, { onDelete: "SET NULL" })
|
||||
user: User;
|
||||
|
||||
@Column({ type: "varchar", nullable: false })
|
||||
@Column({ type: "varchar", nullable: false, unique: true })
|
||||
endpoint: string;
|
||||
|
||||
@Column({ type: "bigint", nullable: true })
|
||||
|
||||
@ -61,3 +61,4 @@ export * from "./UserSettingsProtos";
|
||||
export * from "./ValidRegistrationTokens";
|
||||
export * from "./VoiceState";
|
||||
export * from "./Webhook";
|
||||
export * from "./PushSubscription";
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class MakeEndpointUnique1765129731012 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "idx_push_subscriptions_endpoint"
|
||||
ON "push_subscriptions" ("endpoint");
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS "idx_push_subscriptions_endpoint";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@ -3,27 +3,38 @@ import { yellow } from "picocolors";
|
||||
import { PushSubscription } from "../entities/PushSubscription";
|
||||
import webpush, { PushSubscription as WebPushSubscription } from "web-push";
|
||||
|
||||
let vapidConfigured = false;
|
||||
export let vapidConfigured = false;
|
||||
|
||||
export function configurePush() {
|
||||
const { enabled, email, publicVapidKey, privateVapidKey } = Config.get().webPush;
|
||||
const { enabled, subject, publicVapidKey, privateVapidKey } = Config.get().webPush;
|
||||
|
||||
if (!enabled) {
|
||||
vapidConfigured = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email || !publicVapidKey || !privateVapidKey) {
|
||||
if (!subject || !publicVapidKey || !privateVapidKey) {
|
||||
console.warn("[WebPush]", yellow("VAPID details are missing. Push notifications will be disabled."));
|
||||
vapidConfigured = false;
|
||||
return;
|
||||
}
|
||||
|
||||
webpush.setVapidDetails(email, publicVapidKey, privateVapidKey);
|
||||
webpush.setVapidDetails(subject, publicVapidKey, privateVapidKey);
|
||||
vapidConfigured = true;
|
||||
}
|
||||
|
||||
export function parseSubscription(result: PushSubscription): WebPushSubscription {
|
||||
export async function sendNotification(subscription: PushSubscription, title: string, body: string) {
|
||||
return webpush.sendNotification(parseSubscription(subscription), JSON.stringify({ title, body }), { TTL: 60 }).catch(async (err) => {
|
||||
if (err.statusCode === 404 || err.statusCode === 410) {
|
||||
console.log(`[WebPush] Deleting subscription due to HTTP error`);
|
||||
await PushSubscription.delete({ endpoint: subscription.endpoint });
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function parseSubscription(result: PushSubscription): WebPushSubscription {
|
||||
return {
|
||||
endpoint: result.endpoint,
|
||||
expirationTime: result.expiration_time ?? null,
|
||||
|
||||
@ -48,4 +48,5 @@ export * from "./Application";
|
||||
export * from "./NameValidation";
|
||||
export * from "../../schemas/HelperTypes";
|
||||
export * from "./extensions";
|
||||
export * from "./Random";
|
||||
export * from "./Random";
|
||||
export * from "./WebPush";
|
||||
|
||||
Reference in New Issue
Block a user