security audit fix start

This commit is contained in:
2026-05-28 20:23:38 -04:00
parent 26d9f8b050
commit 469c28fa64
24 changed files with 1741 additions and 555 deletions

View File

@@ -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",
});