All files / src/platform/communications/web-push web-push.service.ts

0% Statements 0/42
0% Branches 0/24
0% Functions 0/5
0% Lines 0/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133                                                                                                                                                                                                                                                                         
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import {
  WebPushSubscription,
  WebPushDelivery,
  WebPushConfiguration,
} from './schemas/web-push.schema';
import * as crypto from 'crypto';
 
@Injectable()
export class WebPushService {
  private readonly logger = new Logger(WebPushService.name);
 
  constructor(
    @InjectModel(WebPushSubscription.name)
    private readonly subscriptionModel: Model<WebPushSubscription>,
    @InjectModel(WebPushDelivery.name)
    private readonly deliveryModel: Model<WebPushDelivery>,
    @InjectModel(WebPushConfiguration.name)
    private readonly configModel: Model<WebPushConfiguration>,
  ) {}
 
  async getOrCreateKeys(
    tenantId: string,
  ): Promise<{ publicKey: string; publicKeyJwk?: any }> {
    let config = await this.configModel.findOne({ tenantId }).exec();
    if (!config) {
      // In production we would generate EC VAPID keys:
      // const keys = webpush.generateVAPIDKeys();
      // For local VM/VPS, we mock it stably:
      const rawPub = crypto.randomBytes(32).toString('base64url');
      const rawPriv = crypto.randomBytes(32).toString('base64url');
      config = await this.configModel.create({
        tenantId,
        publicKey: `B${rawPub}`, // typical VAPID pub format prefix
        privateKey: rawPriv,
      });
    }
    return { publicKey: config.publicKey };
  }
 
  async subscribe(
    tenantId: string,
    userId: string,
    subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
    userAgent?: string,
  ): Promise<WebPushSubscription> {
    const existing = await this.subscriptionModel
      .findOne({ endpoint: subscription.endpoint })
      .exec();
    const doc =
      existing ??
      new this.subscriptionModel({ endpoint: subscription.endpoint });
 
    doc.userId = userId;
    doc.tenantId = tenantId;
    doc.keys = subscription.keys;
    doc.userAgent = userAgent ?? '';
    doc.isActive = true;
 
    await doc.save();
    this.logger.log(`Web push subscription registered for user: ${userId}`);
    return doc;
  }
 
  async unsubscribe(endpoint: string): Promise<void> {
    await this.subscriptionModel
      .findOneAndUpdate({ endpoint }, { isActive: false })
      .exec();
  }
 
  async sendNotification(
    tenantId: string,
    userId: string,
    payload: {
      title: string;
      body: string;
      icon?: string;
      badge?: string;
      tag?: string;
      data?: Record<string, any>;
    },
  ): Promise<void> {
    const subscriptions = await this.subscriptionModel
      .find({ userId, tenantId, isActive: true })
      .exec();
    if (subscriptions.length === 0) {
      this.logger.warn(`No active web push subscriptions for user: ${userId}`);
      return;
    }
 
    for (const sub of subscriptions) {
      try {
        // In production, execute webpush.sendNotification(sub, JSON.stringify(payload));
        this.logger.log(
          `[Web Push Mock] Dispatching to endpoint ${sub.endpoint.substring(0, 40)}... for user ${userId}: "${payload.title}"`,
        );
 
        await this.deliveryModel.create({
          tenantId,
          userId,
          subscriptionEndpoint: sub.endpoint,
          title: payload.title,
          body: payload.body,
          icon: payload.icon,
          badge: payload.badge,
          tag: payload.tag,
          data: payload.data,
          status: 'sent',
        });
      } catch (err: any) {
        this.logger.error(`Failed to send web push: ${err.message}`);
        await this.deliveryModel.create({
          tenantId,
          userId,
          subscriptionEndpoint: sub.endpoint,
          title: payload.title,
          body: payload.body,
          status: 'failed',
          errorMessage: err.message,
        });
 
        if (err.statusCode === 410 || err.statusCode === 404) {
          // subscription expired or invalid
          sub.isActive = false;
          await sub.save();
        }
      }
    }
  }
}