Skip to content

Commit

Permalink
feat(api): add revenuecat webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Sep 22, 2024
1 parent 7906ced commit 4bec9ac
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 21 deletions.
2 changes: 1 addition & 1 deletion apps/api/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { authMiddleware } from './middlewares/auth'
import authApp from './routes/auth'
import budgetsApp from './routes/budgets'
import categoriesApp from './routes/categories'
import clerkWebhooksApp from './routes/clerk-webhooks'
import exchangeRatesApp from './routes/exchange-rates'
import transactionsApp from './routes/transactions'
import usersApp from './routes/users'
import walletsApp from './routes/wallets'
import clerkWebhooksApp from './routes/webhooks/clerk'

export const hono = new Hono()
.get('/health', (c) => c.text('ok'))
Expand Down
57 changes: 41 additions & 16 deletions apps/api/v1/middlewares/webhook-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,52 @@ import { Webhook } from 'svix'
import { getLogger } from '../../lib/log'

export const webhookAuthMiddleware = createMiddleware(async (c, next) => {
const { CLERK_WEBHOOK_SECRET_KEY } = process.env
const { path } = c.req

if (!CLERK_WEBHOOK_SECRET_KEY) {
return c.json({ message: 'CLERK_WEBHOOK_SECRET_KEY is not set' }, 500)
if (path.includes('clerk')) {
const { CLERK_WEBHOOK_SECRET_KEY } = process.env

if (!CLERK_WEBHOOK_SECRET_KEY) {
return c.json({ message: 'CLERK_WEBHOOK_SECRET_KEY is not set' }, 500)
}

const bodyText = await c.req.text()
const svix = new Webhook(CLERK_WEBHOOK_SECRET_KEY)

try {
svix.verify(bodyText, {
'svix-id': c.req.header('svix-id')!,
'svix-timestamp': c.req.header('svix-timestamp')!,
'svix-signature': c.req.header('svix-signature')!,
})
} catch (error) {
const logger = getLogger('webhookAuthMiddleware')
logger.error(error)

return c.json({ success: false, message: `svix validation failed` }, 400)
}

return await next()
}

const bodyText = await c.req.text()
const svix = new Webhook(CLERK_WEBHOOK_SECRET_KEY)
if (path.includes('revenuecat')) {
const { REVENUECAT_WEBHOOK_SECRET } = process.env

if (!REVENUECAT_WEBHOOK_SECRET) {
return c.json({ message: 'REVENUECAT_WEBHOOK_SECRET is not set' }, 500)
}

const authorization = c.req.header('Authorization')

try {
svix.verify(bodyText, {
'svix-id': c.req.header('svix-id')!,
'svix-timestamp': c.req.header('svix-timestamp')!,
'svix-signature': c.req.header('svix-signature')!,
})
} catch (error) {
const logger = getLogger('webhookAuthMiddleware')
logger.error(error)
if (
!authorization ||
authorization !== `Bearer ${REVENUECAT_WEBHOOK_SECRET}`
) {
return c.json({ success: false, message: 'unauthorized' }, 401)
}

return c.json({ message: `svix validation failed` }, 400)
return await next()
}

await next()
return c.json({ success: false, message: 'Not found' }, 404)
})
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { webhookAuthMiddleware } from '../middlewares/webhook-auth'
import { deleteUser } from '../services/user.service'
import { webhookAuthMiddleware } from '../../middlewares/webhook-auth'
import { deleteUser } from '../../services/user.service'

const zClerkUserData = z.object({ id: z.string() }) // Define more fields if needed

Expand All @@ -21,10 +21,13 @@ const router = new Hono()
switch (payload.type) {
case 'user.deleted':
await deleteUser(payload.data.id)
return c.json({ message: 'user deleted' })
return c.json({ success: true, message: 'user deleted' })

default:
return c.json({ message: `${payload.type} is not supported` }, 400)
return c.json(
{ success: false, message: `${payload.type} is not supported` },
400,
)
}
})

Expand Down
41 changes: 41 additions & 0 deletions apps/api/v1/routes/webhooks/revenuecat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { getLogger } from '../../../lib/log'
import { webhookAuthMiddleware } from '../../middlewares/webhook-auth'
import { findUserById, syncUserSubscription } from '../../services/user.service'

const zPayload = z.object({
api_version: z.literal('1.0'),
event: z.object({
app_user_id: z.string(),
environment: z.enum(['SANDBOX', 'PRODUCTION']),
original_app_user_id: z.string(),
}),
})

const router = new Hono()
.use(webhookAuthMiddleware)
.post('/', zValidator('json', zPayload), async (c) => {
const logger = getLogger('webhooks:revenuecat')
const payload = c.req.valid('json')
const { event } = payload

logger.debug('Received payload %o', payload)

const userId = event.original_app_user_id || event.app_user_id
const user = userId ? await findUserById(userId) : null

if (!user) {
logger.warn('User not found for id %s', userId)
return c.json({ success: false, message: 'user not found' }, 404)
}

const syncedUser = await syncUserSubscription(user)

logger.debug('Synced user %o', syncedUser)

return c.json({ success: true })
})

export default router

0 comments on commit 4bec9ac

Please sign in to comment.