fix(billing): block direct public credit topups
This commit is contained in:
@@ -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({
|
export const topUp = mutation({
|
||||||
args: {
|
args: {
|
||||||
amount: v.number(), // Betrag in Cent
|
amount: v.number(), // Betrag in Cent
|
||||||
},
|
},
|
||||||
handler: async (ctx, { amount }) => {
|
handler: async (ctx, { amount }) => {
|
||||||
const user = await requireAuth(ctx);
|
await requireAuth(ctx);
|
||||||
if (amount <= 0) throw new Error("Amount must be positive");
|
|
||||||
|
|
||||||
// Tier-Limit prüfen
|
if (amount <= 0) {
|
||||||
const subscription = await ctx.db
|
throw new ConvexError({
|
||||||
.query("subscriptions")
|
code: "TOPUP_INVALID_AMOUNT",
|
||||||
.withIndex("by_user", (q) => q.eq("userId", user.userId))
|
data: { message: "Amount must be positive" },
|
||||||
.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}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credits gutschreiben
|
throw new ConvexError({
|
||||||
const balance = await ctx.db
|
code: "TOPUP_DISABLED",
|
||||||
.query("creditBalances")
|
data: {
|
||||||
.withIndex("by_user", (q) => q.eq("userId", user.userId))
|
message:
|
||||||
.unique();
|
"Direct top-up credits are disabled. Start checkout via Polar and wait for webhook confirmation.",
|
||||||
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)}€`,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user