Files
falah-mobile/src/app/api/webhooks/polar/route.ts
T

88 lines
2.9 KiB
TypeScript
Raw Normal View History

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { createHmac } from 'crypto'
// Polar product IDs → tier flags
const PRODUCT_TIERS: Record<string, { isPremium: boolean; isPro: boolean }> = {
'a291a3d9-fa82-4e88-ba89-c6005401398f': { isPremium: true, isPro: false }, // Premium
'7f983017-e1f3-440e-be2a-5a5d19c9c7b0': { isPremium: true, isPro: true }, // Pro
}
function verifySignature(body: string, headers: Headers): boolean {
const secret = process.env.POLAR_WEBHOOK_SECRET
if (!secret) return false
// Polar uses Svix: signature is over "id.timestamp.body"
const webhookId = headers.get('webhook-id')
const timestamp = headers.get('webhook-timestamp')
const signature = headers.get('webhook-signature')
if (!webhookId || !timestamp || !signature) return false
const signedContent = `${webhookId}.${timestamp}.${body}`
const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64')
const computed = createHmac('sha256', secretBytes).update(signedContent).digest('base64')
// Signature header may contain multiple: "v1,<sig1> v1,<sig2>"
return signature.split(' ').some(s => s === `v1,${computed}`)
}
export async function POST(req: NextRequest) {
const body = await req.text()
if (process.env.POLAR_WEBHOOK_SECRET && !verifySignature(body, req.headers)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
let event: { type: string; data: any }
try {
event = JSON.parse(body)
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
}
const { type, data } = event
try {
switch (type) {
case 'subscription.created':
case 'subscription.updated': {
const userId = data.metadata?.userId || data.customer?.metadata?.userId
if (!userId) break
const productId = data.product_id || data.product?.id
const tier = productId ? PRODUCT_TIERS[productId] : null
const isActive = ['active', 'trialing'].includes(data.status)
if (isActive && tier) {
await prisma.user.update({
where: { id: userId },
data: tier,
})
} else if (!isActive) {
await prisma.user.update({
where: { id: userId },
data: { isPremium: false, isPro: false },
})
}
break
}
case 'subscription.revoked':
case 'subscription.canceled': {
const userId = data.metadata?.userId || data.customer?.metadata?.userId
if (!userId) break
await prisma.user.update({
where: { id: userId },
data: { isPremium: false, isPro: false },
})
break
}
}
} catch (e) {
console.error('Polar webhook handler error:', e)
return NextResponse.json({ error: 'Handler error' }, { status: 500 })
}
return NextResponse.json({ received: true })
}