feat: ko-fi integration for credit purchases

This commit is contained in:
2026-04-23 11:59:05 -04:00
parent 4a68cc85f3
commit 3dcece5c67
4 changed files with 233 additions and 0 deletions
+112
View File
@@ -6,6 +6,7 @@ import { prisma } from "../utils/prisma";
const router = Router();
const WEBHOOK_SECRET = process.env.TAUTULLI_WEBHOOK_SECRET || "";
const KOFI_VERIFICATION_TOKEN = process.env.KOFI_VERIFICATION_TOKEN || "";
function verifyWebhookSignature(payload: string, signature: string): boolean {
if (!WEBHOOK_SECRET) return true;
@@ -115,4 +116,115 @@ router.post(
}),
);
router.post(
"/kofi",
asyncHandler(async (req, res) => {
const rawData = req.body.data;
if (!rawData) return res.status(400).json({ error: "Missing data" });
let payload: any;
try {
payload = JSON.parse(rawData);
} catch {
return res.status(400).json({ error: "Invalid JSON" });
}
if (payload.verification_token !== KOFI_VERIFICATION_TOKEN) {
return res.status(403).json({ error: "Invalid verification token" });
}
if (payload.type !== "Donation") {
return res.json({ message: "Non-donation event ignored" });
}
const kofiTxId = payload.kofi_transaction_id;
if (!kofiTxId) {
return res.status(400).json({ error: "Missing transaction id" });
}
const existing = await prisma.kofiPayment.findUnique({
where: { kofiTransactionId: kofiTxId },
});
if (existing) {
return res.json({ success: true, alreadyProcessed: true });
}
const amount = parseFloat(payload.amount) || 0;
const creditsGranted = Math.floor(amount * 100);
const email = (payload.email || "").toLowerCase().trim();
if (creditsGranted <= 0) {
return res.status(400).json({ error: "Invalid amount" });
}
const user = email
? await prisma.user.findFirst({
where: {
email: { equals: email, mode: "insensitive" },
},
})
: null;
if (user) {
await prisma.$transaction([
prisma.kofiPayment.create({
data: {
kofiTransactionId: kofiTxId,
email: payload.email || "",
fromName: payload.from_name || "",
amount,
creditsGranted,
message: payload.message || null,
isPublic: payload.is_public ?? true,
currency: payload.currency || "USD",
userId: user.id,
status: "CREDITED",
},
}),
prisma.transaction.create({
data: {
userId: user.id,
type: "PURCHASE",
amount: creditsGranted,
description: `Ko-fi $${amount.toFixed(2)}`,
contentTitle: "Ko-fi Credit Purchase",
},
}),
prisma.user.update({
where: { id: user.id },
data: { totalEarned: { increment: creditsGranted } },
}),
]);
io.to(`user:${user.id}`).emit("credits_earned", {
amount: creditsGranted,
title: "Ko-fi Purchase",
transaction: {
id: kofiTxId,
type: "PURCHASE",
amount: creditsGranted,
contentTitle: "Ko-fi Credit Purchase",
createdAt: new Date(),
},
});
} else {
await prisma.kofiPayment.create({
data: {
kofiTransactionId: kofiTxId,
email: payload.email || "",
fromName: payload.from_name || "",
amount,
creditsGranted,
message: payload.message || null,
isPublic: payload.is_public ?? true,
currency: payload.currency || "USD",
status: "UNCLAIMED",
},
});
}
res.json({ success: true, credited: !!user, creditsGranted });
}),
);
export { router as webhookRouter };