File size: 4,144 Bytes
b43e552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b0c22b
b43e552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8280d7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b43e552
 
 
 
7b0c22b
b43e552
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b0c22b
b43e552
 
 
 
 
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
import webpush from 'web-push';
import { prisma } from './prisma';
import { logger } from '../logger';

// VAPID keys should be in .env. If not, we log a warning.
const publicVapidKey = process.env.VAPID_PUBLIC_KEY;
const privateVapidKey = process.env.VAPID_PRIVATE_KEY;
const contactEmail = process.env.VAPID_EMAIL || 'mailto:support@xamle.studio';

if (publicVapidKey && privateVapidKey) {
    webpush.setVapidDetails(contactEmail, publicVapidKey, privateVapidKey);
} else {
    logger.warn('[PUSH-SERVICE] VAPID keys are missing. Push notifications will not work.');
    // To generate keys, you can use: webpush.generateVAPIDKeys()
}

export const pushService = {
    /**
     * Store a new subscription for a user
     */
    async subscribe(userId: string, organizationId: string, subscription: any) {
        return prisma.pushSubscription.upsert({
            where: { endpoint: subscription.endpoint },
            update: {
                userId,
                organizationId,
                p256dh: subscription.keys.p256dh,
                auth: subscription.keys.auth
            },
            create: {
                userId,
                organizationId,
                endpoint: subscription.endpoint,
                p256dh: subscription.keys.p256dh,
                auth: subscription.keys.auth
            }
        });
    },

    /**
     * Send a notification to all active subscriptions of a specific user
     */
    async sendToUser(userId: string, organizationId: string, title: string, body: string, icon?: string, data?: Record<string, unknown>) {
        const subscriptions = await prisma.pushSubscription.findMany({ where: { userId, organizationId } });

        const payload = JSON.stringify({
            title,
            body,
            icon: icon || 'https://xamle.studio/logo.png',
            data,
            timestamp: Date.now(),
        });

        const results = await Promise.allSettled(
            subscriptions.map((sub: any) => {
                const pushConfig = { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } };
                return webpush.sendNotification(pushConfig, payload);
            })
        );

        for (let i = 0; i < results.length; i++) {
            if (results[i].status === 'rejected') {
                const error = (results[i] as PromiseRejectedResult).reason;
                if (error.statusCode === 410 || error.statusCode === 404) {
                    await prisma.pushSubscription.delete({ where: { id: subscriptions[i].id } }).catch(() => {});
                }
            }
        }
    },

    /**
     * Send a notification to all active subscriptions of an organization
     */
    async notifyOrganization(organizationId: string, title: string, body: string, icon?: string) {
        const subscriptions = await prisma.pushSubscription.findMany({
            where: { organizationId }
        });

        const payload = JSON.stringify({
            title,
            body,
            icon: icon || 'https://xamle.studio/logo.png',
            timestamp: Date.now()
        });

        const results = await Promise.allSettled(
            subscriptions.map((sub: any) => {
                const pushConfig = {
                    endpoint: sub.endpoint,
                    keys: {
                        p256dh: sub.p256dh,
                        auth: sub.auth
                    }
                };

                return webpush.sendNotification(pushConfig, payload);
            })
        );

        // Clean up failed subscriptions (e.g. expired or unsubscribed)
        for (let i = 0; i < results.length; i++) {
            if (results[i].status === 'rejected') {
                const error = (results[i] as PromiseRejectedResult).reason;
                if (error.statusCode === 410 || error.statusCode === 404) {
                    logger.info(`[PUSH-SERVICE] Removing expired subscription: ${subscriptions[i].endpoint}`);
                    await prisma.pushSubscription.delete({ where: { id: subscriptions[i].id } }).catch(() => {});
                }
            }
        }
    }
};