Files
iistwin/server/services/web-push.service.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

161 lines
5.0 KiB
TypeScript

import webpush from 'web-push';
import { storage } from '../storage';
const VAPID_KEY_CONFIG_KEY = 'vapid_public_key';
interface WebPushSubscription {
endpoint: string;
keys: {
p256dh: string;
auth: string;
};
}
class WebPushService {
private initialized = false;
private vapidPublicKey: string = '';
private vapidPrivateKey: string = '';
constructor() {
this.initialize();
}
private initialize() {
const publicKey = process.env.VAPID_PUBLIC_KEY;
const privateKey = process.env.VAPID_PRIVATE_KEY;
if (!publicKey || !privateKey) {
console.warn('[WebPush] WARNING: VAPID_PUBLIC_KEY and/or VAPID_PRIVATE_KEY are not set.');
console.warn('[WebPush] Generating temporary VAPID keys for this session only.');
console.warn('[WebPush] All existing push subscriptions will be invalidated on every server restart!');
console.warn('[WebPush] To fix this, generate permanent keys and set them as environment variables:');
console.warn('[WebPush] npx web-push generate-vapid-keys');
console.warn('[WebPush] Then set VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY in your environment secrets.');
const keys = webpush.generateVAPIDKeys();
this.vapidPublicKey = keys.publicKey;
this.vapidPrivateKey = keys.privateKey;
} else {
this.vapidPublicKey = publicKey;
this.vapidPrivateKey = privateKey;
}
webpush.setVapidDetails(
'mailto:admin@iistwin.ru',
this.vapidPublicKey,
this.vapidPrivateKey
);
this.initialized = true;
console.log('[WebPush] Service initialized');
}
async checkVapidKeyRotation(): Promise<void> {
try {
const storedKey = await storage.getSystemConfig(VAPID_KEY_CONFIG_KEY);
if (storedKey === null) {
await storage.setSystemConfig(VAPID_KEY_CONFIG_KEY, this.vapidPublicKey);
console.log('[WebPush] VAPID public key stored in system config for future rotation checks.');
return;
}
if (storedKey !== this.vapidPublicKey) {
console.warn('[WebPush] VAPID public key has changed! Previous key does not match current key.');
console.warn('[WebPush] Clearing all existing web push subscriptions — they are no longer valid.');
const count = await storage.clearAllWebPushSubscriptions();
console.warn(`[WebPush] Removed ${count} stale push subscription(s).`);
await storage.setSystemConfig(VAPID_KEY_CONFIG_KEY, this.vapidPublicKey);
console.log('[WebPush] System config updated with new VAPID public key.');
}
} catch (err) {
console.error('[WebPush] Error during VAPID key rotation check:', err);
}
}
getVapidPublicKey(): string {
return this.vapidPublicKey;
}
async registerSubscription(
userId: number,
organizationId: number,
subscription: WebPushSubscription
): Promise<void> {
await storage.registerWebPushSubscription(userId, organizationId, subscription.endpoint, JSON.stringify(subscription));
console.log(`[WebPush] Subscription registered for user ${userId}`);
}
async unregisterSubscription(endpoint: string): Promise<void> {
await storage.unregisterWebPushSubscription(endpoint);
console.log(`[WebPush] Subscription unregistered: ${endpoint.substring(0, 50)}...`);
}
async sendNotification(
subscription: WebPushSubscription,
payload: {
title?: string;
body?: string;
icon?: string;
url?: string;
tag?: string;
data?: Record<string, any>;
type?: string;
[key: string]: any;
}
): Promise<boolean> {
if (!this.initialized) {
console.log('[WebPush] Service not initialized');
return false;
}
try {
const result = await webpush.sendNotification(
subscription,
JSON.stringify(payload)
);
console.log(`[WebPush] FCM response status=${result.statusCode}, body=${result.body?.substring?.(0, 200) ?? result.body}`);
return true;
} catch (error: any) {
if (error.statusCode === 410 || error.statusCode === 404) {
console.log('[WebPush] Subscription expired, removing...');
await this.unregisterSubscription(subscription.endpoint);
} else {
console.error('[WebPush] Send error:', error.message);
}
return false;
}
}
async sendToUser(
userId: number,
organizationId: number,
payload: {
title?: string;
body?: string;
icon?: string;
url?: string;
tag?: string;
data?: Record<string, any>;
type?: string;
[key: string]: any;
}
): Promise<number> {
const subscriptions = await storage.getWebPushSubscriptions(userId, organizationId);
let sent = 0;
for (const sub of subscriptions) {
try {
const subscription = JSON.parse(sub.subscription);
const success = await this.sendNotification(subscription, payload);
if (success) sent++;
} catch (error) {
console.error('[WebPush] Parse error for subscription:', error);
}
}
return sent;
}
}
export const webPushService = new WebPushService();