From c094f6c80bb5a655d46fc6e7da68636a5b68cb23 Mon Sep 17 00:00:00 2001 From: Matthias Meister Date: Fri, 3 Apr 2026 17:40:22 +0200 Subject: [PATCH] fix(billing): block direct public credit topups --- convex/credits.ts | 67 ++++++++++++----------------------------------- 1 file changed, 17 insertions(+), 50 deletions(-) diff --git a/convex/credits.ts b/convex/credits.ts index 0f40633..0388636 100644 --- a/convex/credits.ts +++ b/convex/credits.ts @@ -776,65 +776,32 @@ export const activateSubscription = internalMutation({ }); /** - * Credits nachkaufen (Top-Up). + * Legacy Top-Up Endpoint. + * + * Security hardening (P0.1): direkte Credit-Gutschriften ueber einen oeffentlichen + * Mutationspfad sind deaktiviert. Credits duerfen nur ueber verifizierte + * Payment-Webhooks (convex/polar.ts) verbucht werden. */ export const topUp = mutation({ args: { amount: v.number(), // Betrag in Cent }, handler: async (ctx, { amount }) => { - const user = await requireAuth(ctx); - if (amount <= 0) throw new Error("Amount must be positive"); + await requireAuth(ctx); - // Tier-Limit prüfen - const subscription = await ctx.db - .query("subscriptions") - .withIndex("by_user", (q) => q.eq("userId", user.userId)) - .order("desc") - .first(); - const tier = (subscription?.tier ?? "free") as Tier; - const config = TIER_CONFIG[tier]; - - // Monatliches Top-Up-Limit prüfen - const monthStart = new Date(); - monthStart.setDate(1); - monthStart.setHours(0, 0, 0, 0); - - const monthlyTopUps = await ctx.db - .query("creditTransactions") - .withIndex("by_user_type", (q) => - q.eq("userId", user.userId).eq("type", "topup") - ) - .collect(); - - const thisMonthTopUps = monthlyTopUps - .filter((t) => t._creationTime >= monthStart.getTime()) - .reduce((sum, t) => sum + t.amount, 0); - - if (thisMonthTopUps + amount > config.topUpLimit) { - throw new Error( - `Monthly top-up limit reached. Limit: ${config.topUpLimit}, used: ${thisMonthTopUps}` - ); + if (amount <= 0) { + throw new ConvexError({ + code: "TOPUP_INVALID_AMOUNT", + data: { message: "Amount must be positive" }, + }); } - // Credits gutschreiben - const balance = await ctx.db - .query("creditBalances") - .withIndex("by_user", (q) => q.eq("userId", user.userId)) - .unique(); - if (!balance) throw new Error("No credit balance found"); - - await ctx.db.patch(balance._id, { - balance: balance.balance + amount, - updatedAt: Date.now(), - }); - - await ctx.db.insert("creditTransactions", { - userId: user.userId, - amount, - type: "topup", - status: "committed", - description: `Credit-Nachkauf — ${(amount / 100).toFixed(2)}€`, + throw new ConvexError({ + code: "TOPUP_DISABLED", + data: { + message: + "Direct top-up credits are disabled. Start checkout via Polar and wait for webhook confirmation.", + }, }); }, });