Files
RFP_Finanzen/src/lib/push-notifications.ts
jan 6dec4b8a10
All checks were successful
CI / Build and Deploy (push) Successful in 2m30s
UI Push Deep Links und Drive Diagnose verbessern
2026-05-06 00:11:33 +02:00

153 lines
4.1 KiB
TypeScript

import webpush from "web-push";
import type { ApprovalType, BudgetReleaseNotifyTarget } from "@prisma/client";
import { approvalLabel } from "@/lib/domain";
import prisma from "@/lib/prisma";
type PushTargetExpense = {
id: string;
title: string;
amount: number;
workingGroupId: string;
};
type PushTargetBudgetRelease = {
id: string;
name: string;
workingGroupId: string;
workingGroupName: string;
releasedAmount: number;
};
function configureWebPush() {
const publicKey = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY;
const privateKey = process.env.VAPID_PRIVATE_KEY;
const subject = process.env.VAPID_SUBJECT;
if (!publicKey || !privateKey || !subject) {
return false;
}
webpush.setVapidDetails(subject, publicKey, privateKey);
return true;
}
export function getApprovalRole(approvalType: ApprovalType) {
switch (approvalType) {
case "CHAIR_A":
return "ORGA";
case "CHAIR_B":
return "BOARD";
case "FINANCE":
return "FINANCE";
}
}
export async function notifyApprovalRequest(expense: PushTargetExpense, approvalTypes: ApprovalType[]) {
if (!configureWebPush() || approvalTypes.length === 0) {
return;
}
const roles = approvalTypes.map(getApprovalRole);
const subscriptions = await prisma.pushSubscription.findMany({
where: {
user: {
role: {
in: roles
}
}
},
include: {
user: {
select: {
role: true
}
}
}
});
await Promise.all(
subscriptions.map(async (subscription) => {
const approvalType = approvalTypes.find((type) => getApprovalRole(type) === subscription.user.role);
const payload = JSON.stringify({
title: "Freigabe angefragt",
body: `${expense.title} (${expense.amount.toFixed(2)} EUR) braucht ${approvalType ? approvalLabel(approvalType) : "deine Freigabe"}.`,
url: `/?expense=${encodeURIComponent(expense.id)}&group=${encodeURIComponent(expense.workingGroupId)}`,
tag: `approval-${expense.id}-${subscription.user.role}`
});
try {
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.p256dh,
auth: subscription.auth
}
},
payload
);
} catch (error) {
const statusCode = typeof error === "object" && error && "statusCode" in error ? error.statusCode : null;
if (statusCode === 404 || statusCode === 410) {
await prisma.pushSubscription.delete({
where: {
endpoint: subscription.endpoint
}
});
}
}
})
);
}
export async function notifyBudgetRelease(budget: PushTargetBudgetRelease, target: BudgetReleaseNotifyTarget) {
if (!configureWebPush()) {
return;
}
const subscriptions = await prisma.pushSubscription.findMany({
where: {
user: {
workingGroupId: budget.workingGroupId,
...(target === "GROUP_MEMBERS_ONLY" ? { role: "MEMBER" } : {})
}
}
});
await Promise.all(
subscriptions.map(async (subscription) => {
const payload = JSON.stringify({
title: "Budget freigegeben",
body: `${budget.workingGroupName}: ${budget.name} wurde mit ${budget.releasedAmount.toFixed(2)} EUR freigegeben.`,
url: `/?budget=${encodeURIComponent(budget.id)}&group=${encodeURIComponent(budget.workingGroupId)}`,
tag: `budget-release-${budget.id}`
});
try {
await webpush.sendNotification(
{
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.p256dh,
auth: subscription.auth
}
},
payload
);
} catch (error) {
const statusCode = typeof error === "object" && error && "statusCode" in error ? error.statusCode : null;
if (statusCode === 404 || statusCode === 410) {
await prisma.pushSubscription.delete({
where: {
endpoint: subscription.endpoint
}
});
}
}
})
);
}