security audit fix start
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { safeParse } from "valibot";
|
||||
import { db } from "~/server/db";
|
||||
import { stripe } from "~/server/stripe";
|
||||
import { users } from "~/server/db/schema/auth";
|
||||
import { subscriptions } from "~/server/db/schema/subscription";
|
||||
import type Stripe from "stripe";
|
||||
import {
|
||||
CheckoutSessionSchema,
|
||||
SubscriptionSchema,
|
||||
InvoiceSchema,
|
||||
} from "~/server/api/schemas/webhook";
|
||||
|
||||
type Tier = "basic" | "plus" | "premium";
|
||||
|
||||
@@ -139,19 +145,46 @@ export async function updateSubscriptionInDB(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function handleWebhookEvent(event: Stripe.Event) {
|
||||
const obj = event.data.object as unknown as Record<string, unknown>;
|
||||
function safeParseSubscription(obj: unknown) {
|
||||
const result = safeParse(SubscriptionSchema, obj);
|
||||
if (!result.success) {
|
||||
console.error(`[webhook] Failed to parse subscription data: ${result.issues?.map((i) => i.message).join(", ")}`);
|
||||
return null;
|
||||
}
|
||||
return result.output;
|
||||
}
|
||||
|
||||
function safeParseCheckoutSession(obj: unknown) {
|
||||
const result = safeParse(CheckoutSessionSchema, obj);
|
||||
if (!result.success) {
|
||||
console.error(`[webhook] Failed to parse checkout session data: ${result.issues?.map((i) => i.message).join(", ")}`);
|
||||
return null;
|
||||
}
|
||||
return result.output;
|
||||
}
|
||||
|
||||
function safeParseInvoice(obj: unknown) {
|
||||
const result = safeParse(InvoiceSchema, obj);
|
||||
if (!result.success) {
|
||||
console.error(`[webhook] Failed to parse invoice data: ${result.issues?.map((i) => i.message).join(", ")}`);
|
||||
return null;
|
||||
}
|
||||
return result.output;
|
||||
}
|
||||
|
||||
export async function handleWebhookEvent(event: Stripe.Event) {
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
const session = obj as unknown as Stripe.Checkout.Session;
|
||||
const session = safeParseCheckoutSession(event.data.object);
|
||||
if (!session) break;
|
||||
|
||||
const userId = session.metadata?.userId;
|
||||
if (!userId || !session.subscription) break;
|
||||
|
||||
const stripeSub = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string,
|
||||
);
|
||||
const sub = stripeSub as unknown as Record<string, unknown>;
|
||||
const stripeSub = await stripe.subscriptions.retrieve(session.subscription);
|
||||
|
||||
// Fetch fresh subscription data from Stripe for accurate fields
|
||||
const subData = stripeSub as unknown as Record<string, unknown>;
|
||||
|
||||
await db.insert(subscriptions).values({
|
||||
userId,
|
||||
@@ -159,65 +192,76 @@ export async function handleWebhookEvent(event: Stripe.Event) {
|
||||
tier: mapStripeProductToTier(
|
||||
stripeSub.items.data[0]?.price?.id ?? "",
|
||||
),
|
||||
status: sub.status as typeof subscriptions.$inferSelect.status,
|
||||
currentPeriodStart: new Date((sub.current_period_start as number) * 1000),
|
||||
currentPeriodEnd: new Date((sub.current_period_end as number) * 1000),
|
||||
cancelAtPeriodEnd: sub.cancel_at_period_end as boolean,
|
||||
status: (subData.status as typeof subscriptions.$inferSelect.status) ?? "active",
|
||||
currentPeriodStart: subData.current_period_start
|
||||
? new Date((subData.current_period_start as number) * 1000)
|
||||
: undefined,
|
||||
currentPeriodEnd: subData.current_period_end
|
||||
? new Date((subData.current_period_end as number) * 1000)
|
||||
: undefined,
|
||||
cancelAtPeriodEnd: Boolean(subData.cancel_at_period_end),
|
||||
}).onConflictDoNothing();
|
||||
break;
|
||||
}
|
||||
|
||||
case "invoice.paid": {
|
||||
const invoice = obj;
|
||||
if (!invoice.subscription) break;
|
||||
const invoice = safeParseInvoice(event.data.object);
|
||||
if (!invoice?.subscription) break;
|
||||
|
||||
await updateSubscriptionInDB(invoice.subscription as string, {
|
||||
await updateSubscriptionInDB(invoice.subscription, {
|
||||
status: "active",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "invoice.payment_failed": {
|
||||
const invoice = obj;
|
||||
if (!invoice.subscription) break;
|
||||
const invoice = safeParseInvoice(event.data.object);
|
||||
if (!invoice?.subscription) break;
|
||||
|
||||
await updateSubscriptionInDB(invoice.subscription as string, {
|
||||
await updateSubscriptionInDB(invoice.subscription, {
|
||||
status: "past_due",
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "customer.subscription.updated": {
|
||||
const stripeSub = obj as unknown as Stripe.Subscription;
|
||||
const userId = stripeSub.metadata?.userId;
|
||||
const sub = stripeSub as unknown as Record<string, unknown>;
|
||||
const validatedSub = safeParseSubscription(event.data.object);
|
||||
|
||||
if (!validatedSub) break;
|
||||
|
||||
const userId = validatedSub.metadata?.userId;
|
||||
if (!userId) {
|
||||
const [existingSub] = await db
|
||||
.select()
|
||||
.from(subscriptions)
|
||||
.where(eq(subscriptions.stripeId, stripeSub.id))
|
||||
.where(eq(subscriptions.stripeId, validatedSub.id))
|
||||
.limit(1);
|
||||
|
||||
if (!existingSub) break;
|
||||
}
|
||||
|
||||
const tier = stripeSub.items.data[0]?.price?.id
|
||||
? mapStripeProductToTier(stripeSub.items.data[0].price.id)
|
||||
const tier = validatedSub.items?.data?.price?.id
|
||||
? mapStripeProductToTier(validatedSub.items.data.price.id)
|
||||
: undefined;
|
||||
|
||||
await updateSubscriptionInDB(stripeSub.id, {
|
||||
await updateSubscriptionInDB(validatedSub.id, {
|
||||
tier,
|
||||
status: sub.status as string,
|
||||
currentPeriodStart: new Date((sub.current_period_start as number) * 1000),
|
||||
currentPeriodEnd: new Date((sub.current_period_end as number) * 1000),
|
||||
cancelAtPeriodEnd: sub.cancel_at_period_end as boolean,
|
||||
status: validatedSub.status ?? undefined,
|
||||
currentPeriodStart: validatedSub.current_period_start
|
||||
? new Date(validatedSub.current_period_start * 1000)
|
||||
: undefined,
|
||||
currentPeriodEnd: validatedSub.current_period_end
|
||||
? new Date(validatedSub.current_period_end * 1000)
|
||||
: undefined,
|
||||
cancelAtPeriodEnd: validatedSub.cancel_at_period_end ?? undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "customer.subscription.deleted": {
|
||||
const stripeSub = obj as unknown as Stripe.Subscription;
|
||||
const stripeSub = safeParseSubscription(event.data.object);
|
||||
if (!stripeSub) break;
|
||||
|
||||
await updateSubscriptionInDB(stripeSub.id, {
|
||||
status: "canceled",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user