mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
feat: Add Cloudflare StreamPlayer component and update schema with billing and access control
- Introduced `CloudflareStreamPlayer` React component for embedding streams - Updated `package.json` with new dependencies: `@cloudflare/stream-react` and `stripe` - Extended database schema with user tiers, Stripe billing, storage, and archive management - Added access control logic in `access.ts` for user tiers and feature permissions - Enhanced billing logic with archive storage limits and subscription checks
This commit is contained in:
201
packages/web/src/routes/api/stripe/webhooks.ts
Normal file
201
packages/web/src/routes/api/stripe/webhooks.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { getStripe, getStripeConfig } from "@/lib/stripe"
|
||||
import { db } from "@/db/connection"
|
||||
import { stripe_customers, stripe_subscriptions, storage_usage } from "@/db/schema"
|
||||
import { eq, and } from "drizzle-orm"
|
||||
import type Stripe from "stripe"
|
||||
|
||||
// Archive subscription limits
|
||||
const ARCHIVE_LIMITS = {
|
||||
archives: 10,
|
||||
storageBytes: 1073741824, // 1GB
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/api/stripe/webhooks")({
|
||||
server: {
|
||||
handlers: {
|
||||
POST: async ({ request }) => {
|
||||
const stripe = getStripe()
|
||||
const { webhookSecret } = getStripeConfig()
|
||||
|
||||
if (!stripe || !webhookSecret) {
|
||||
console.error("[stripe] Stripe not configured")
|
||||
return new Response("Stripe not configured", { status: 500 })
|
||||
}
|
||||
|
||||
const body = await request.text()
|
||||
const signature = request.headers.get("stripe-signature")
|
||||
|
||||
if (!signature) {
|
||||
return new Response("Missing stripe-signature header", { status: 400 })
|
||||
}
|
||||
|
||||
let event: Stripe.Event
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
|
||||
} catch (err) {
|
||||
console.error("[stripe] Webhook signature verification failed:", err)
|
||||
return new Response("Invalid signature", { status: 400 })
|
||||
}
|
||||
|
||||
const database = db()
|
||||
|
||||
try {
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed": {
|
||||
const session = event.data.object as Stripe.Checkout.Session
|
||||
console.log("[stripe] Checkout completed:", session.id)
|
||||
|
||||
if (session.mode === "subscription" && session.subscription) {
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
)
|
||||
await handleSubscriptionCreated(database, subscription)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "customer.subscription.created":
|
||||
case "customer.subscription.updated": {
|
||||
const subscription = event.data.object as Stripe.Subscription
|
||||
console.log(`[stripe] Subscription ${event.type}:`, subscription.id)
|
||||
await handleSubscriptionCreated(database, subscription)
|
||||
break
|
||||
}
|
||||
|
||||
case "customer.subscription.deleted": {
|
||||
const subscription = event.data.object as Stripe.Subscription
|
||||
console.log("[stripe] Subscription deleted:", subscription.id)
|
||||
await handleSubscriptionDeleted(database, subscription)
|
||||
break
|
||||
}
|
||||
|
||||
case "invoice.payment_succeeded": {
|
||||
const invoice = event.data.object as Stripe.Invoice
|
||||
console.log("[stripe] Invoice paid:", invoice.id)
|
||||
break
|
||||
}
|
||||
|
||||
case "invoice.payment_failed": {
|
||||
const invoice = event.data.object as Stripe.Invoice
|
||||
console.log("[stripe] Invoice payment failed:", invoice.id)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.log(`[stripe] Unhandled event type: ${event.type}`)
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ received: true }), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[stripe] Webhook handler error:", error)
|
||||
return new Response("Webhook handler error", { status: 500 })
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
async function handleSubscriptionCreated(
|
||||
database: ReturnType<typeof db>,
|
||||
subscription: Stripe.Subscription
|
||||
) {
|
||||
const customerId = subscription.customer as string
|
||||
const priceId = subscription.items.data[0]?.price.id
|
||||
|
||||
// Find user by Stripe customer ID
|
||||
const [customer] = await database
|
||||
.select()
|
||||
.from(stripe_customers)
|
||||
.where(eq(stripe_customers.stripe_customer_id, customerId))
|
||||
.limit(1)
|
||||
|
||||
if (!customer) {
|
||||
console.error("[stripe] No customer found for:", customerId)
|
||||
return
|
||||
}
|
||||
|
||||
// Upsert subscription
|
||||
const existing = await database
|
||||
.select()
|
||||
.from(stripe_subscriptions)
|
||||
.where(eq(stripe_subscriptions.stripe_subscription_id, subscription.id))
|
||||
.limit(1)
|
||||
|
||||
// Period dates
|
||||
const item = subscription.items.data[0]
|
||||
const periodStart = item?.current_period_start
|
||||
? new Date(item.current_period_start * 1000)
|
||||
: new Date()
|
||||
const periodEnd = item?.current_period_end
|
||||
? new Date(item.current_period_end * 1000)
|
||||
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
|
||||
|
||||
const subscriptionData = {
|
||||
stripe_subscription_id: subscription.id,
|
||||
stripe_customer_id: customerId,
|
||||
stripe_price_id: priceId,
|
||||
status: subscription.status,
|
||||
current_period_start: periodStart,
|
||||
current_period_end: periodEnd,
|
||||
cancel_at_period_end: subscription.cancel_at_period_end,
|
||||
updated_at: new Date(),
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
await database
|
||||
.update(stripe_subscriptions)
|
||||
.set(subscriptionData)
|
||||
.where(eq(stripe_subscriptions.stripe_subscription_id, subscription.id))
|
||||
} else {
|
||||
await database.insert(stripe_subscriptions).values({
|
||||
user_id: customer.user_id,
|
||||
...subscriptionData,
|
||||
})
|
||||
}
|
||||
|
||||
// Create or update storage usage record for this billing period
|
||||
const [existingUsage] = await database
|
||||
.select()
|
||||
.from(storage_usage)
|
||||
.where(
|
||||
and(
|
||||
eq(storage_usage.user_id, customer.user_id),
|
||||
eq(storage_usage.period_start, periodStart)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!existingUsage) {
|
||||
await database.insert(storage_usage).values({
|
||||
user_id: customer.user_id,
|
||||
archives_used: 0,
|
||||
archives_limit: ARCHIVE_LIMITS.archives,
|
||||
storage_bytes_used: 0,
|
||||
storage_bytes_limit: ARCHIVE_LIMITS.storageBytes,
|
||||
period_start: periodStart,
|
||||
period_end: periodEnd,
|
||||
})
|
||||
console.log(`[stripe] Created storage usage record for user ${customer.user_id}`)
|
||||
}
|
||||
|
||||
console.log(`[stripe] Subscription synced for user ${customer.user_id}`)
|
||||
}
|
||||
|
||||
async function handleSubscriptionDeleted(
|
||||
database: ReturnType<typeof db>,
|
||||
subscription: Stripe.Subscription
|
||||
) {
|
||||
await database
|
||||
.update(stripe_subscriptions)
|
||||
.set({
|
||||
status: "canceled",
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.where(eq(stripe_subscriptions.stripe_subscription_id, subscription.id))
|
||||
|
||||
console.log(`[stripe] Subscription ${subscription.id} marked as canceled`)
|
||||
}
|
||||
Reference in New Issue
Block a user