diff --git a/apps/api/src/hooks/client.hook.ts b/apps/api/src/hooks/client.hook.ts new file mode 100644 index 00000000..8f9d5546 --- /dev/null +++ b/apps/api/src/hooks/client.hook.ts @@ -0,0 +1,26 @@ +import { SdkAuthError, validateSdkRequest } from '@/utils/auth'; +import type { TrackHandlerPayload } from '@openpanel/sdk'; +import type { + FastifyReply, + FastifyRequest, + HookHandlerDoneFunction, +} from 'fastify'; + +export async function clientHook( + req: FastifyRequest<{ + Body: TrackHandlerPayload; + }>, + reply: FastifyReply, +) { + try { + const client = await validateSdkRequest(req.headers); + req.projectId = client.projectId; + req.client = client; + } catch (error) { + if (error instanceof SdkAuthError) { + return reply.status(401).send(error.message); + } + + return reply.status(500).send('Internal server error'); + } +} diff --git a/apps/api/src/hooks/is-bot.hook.ts b/apps/api/src/hooks/is-bot.hook.ts new file mode 100644 index 00000000..84419b02 --- /dev/null +++ b/apps/api/src/hooks/is-bot.hook.ts @@ -0,0 +1,51 @@ +import { isBot } from '@/bots'; +import { createBotEvent } from '@openpanel/db'; +import type { TrackHandlerPayload } from '@openpanel/sdk'; +import type { FastifyReply, FastifyRequest } from 'fastify'; + +type DeprecatedEventPayload = { + name: string; + properties: Record; + timestamp: string; +}; + +export async function isBotHook( + req: FastifyRequest<{ + Body: TrackHandlerPayload | DeprecatedEventPayload; + }>, + reply: FastifyReply, +) { + const bot = req.headers['user-agent'] + ? isBot(req.headers['user-agent']) + : null; + + if (bot && req.client?.projectId) { + if ('type' in req.body && req.body.type === 'track') { + const path = (req.body.payload.properties?.__path || + req.body.payload.properties?.path) as string | undefined; + if (path) { + await createBotEvent({ + ...bot, + projectId: req.client.projectId, + path: path ?? '', + createdAt: new Date(), + }); + } + // Handle deprecated events (v1) + } else if ('name' in req.body && 'properties' in req.body) { + const path = (req.body.properties?.__path || req.body.properties?.path) as + | string + | undefined; + if (path) { + await createBotEvent({ + ...bot, + projectId: req.client.projectId, + path: path ?? '', + createdAt: new Date(), + }); + } + } + + return reply.status(202).send('OK'); + } +} diff --git a/apps/api/src/routes/event.router.ts b/apps/api/src/routes/event.router.ts index 914ae504..8dd644f9 100644 --- a/apps/api/src/routes/event.router.ts +++ b/apps/api/src/routes/event.router.ts @@ -1,58 +1,12 @@ -import { isBot } from '@/bots'; import * as controller from '@/controllers/event.controller'; -import { SdkAuthError, validateSdkRequest } from '@/utils/auth'; -import { logger } from '@/utils/logger'; -import type { FastifyPluginCallback, FastifyRequest } from 'fastify'; +import type { FastifyPluginCallback } from 'fastify'; -import { createBotEvent } from '@openpanel/db'; -import type { PostEventPayload } from '@openpanel/sdk'; +import { clientHook } from '@/hooks/client.hook'; +import { isBotHook } from '@/hooks/is-bot.hook'; const eventRouter: FastifyPluginCallback = (fastify, opts, done) => { - fastify.addHook( - 'preHandler', - async ( - req: FastifyRequest<{ - Body: PostEventPayload; - }>, - reply, - ) => { - try { - const client = await validateSdkRequest(req.headers).catch((error) => { - if (error instanceof SdkAuthError) { - return reply.status(401).send(error.message); - } - logger.error('Failed to validate sdk request', { error }); - return reply.status(401).send('Unknown validation error'); - }); - - if (!client?.projectId) { - return reply.status(401).send('No project found for this client'); - } - req.projectId = client.projectId; - req.client = client; - - const bot = req.headers['user-agent'] - ? isBot(req.headers['user-agent']) - : null; - - if (bot) { - const path = (req.body?.properties?.__path || - req.body?.properties?.path) as string | undefined; - await createBotEvent({ - ...bot, - projectId: client.projectId, - path: path ?? '', - createdAt: new Date(req.body?.timestamp), - }); - reply.status(202).send('OK'); - } - } catch (error) { - logger.error('Failed to create bot event', { error }); - reply.status(401).send(); - return; - } - }, - ); + fastify.addHook('preHandler', clientHook); + fastify.addHook('preHandler', isBotHook); fastify.route({ method: 'POST', diff --git a/apps/api/src/routes/profile.router.ts b/apps/api/src/routes/profile.router.ts index da3db8e8..6d88c62c 100644 --- a/apps/api/src/routes/profile.router.ts +++ b/apps/api/src/routes/profile.router.ts @@ -1,39 +1,11 @@ -import { isBot } from '@/bots'; import * as controller from '@/controllers/profile.controller'; -import { SdkAuthError, validateSdkRequest } from '@/utils/auth'; -import { logger } from '@/utils/logger'; +import { clientHook } from '@/hooks/client.hook'; +import { isBotHook } from '@/hooks/is-bot.hook'; import type { FastifyPluginCallback } from 'fastify'; const eventRouter: FastifyPluginCallback = (fastify, opts, done) => { - fastify.addHook('preHandler', async (req, reply) => { - try { - const client = await validateSdkRequest(req.headers).catch((error) => { - if (error instanceof SdkAuthError) { - return reply.status(401).send(error.message); - } - logger.error('Failed to validate sdk request', { error }); - return reply.status(401).send('Unknown validation error'); - }); - - if (!client?.projectId) { - return reply.status(401).send('No project found for this client'); - } - req.projectId = client.projectId; - req.client = client; - - const bot = req.headers['user-agent'] - ? isBot(req.headers['user-agent']) - : null; - - if (bot) { - return reply.status(202).send('OK'); - } - } catch (error) { - logger.error('Failed to create bot event', { error }); - reply.status(401).send(); - return; - } - }); + fastify.addHook('preHandler', clientHook); + fastify.addHook('preHandler', isBotHook); fastify.route({ method: 'POST', diff --git a/apps/api/src/routes/track.router.ts b/apps/api/src/routes/track.router.ts index c8ce3fe1..1f0de440 100644 --- a/apps/api/src/routes/track.router.ts +++ b/apps/api/src/routes/track.router.ts @@ -1,62 +1,16 @@ import { isBot } from '@/bots'; import { handler } from '@/controllers/track.controller'; import { SdkAuthError, validateSdkRequest } from '@/utils/auth'; -import { logger } from '@/utils/logger'; import type { FastifyPluginCallback, FastifyRequest } from 'fastify'; +import { clientHook } from '@/hooks/client.hook'; +import { isBotHook } from '@/hooks/is-bot.hook'; import { createBotEvent } from '@openpanel/db'; import type { TrackHandlerPayload } from '@openpanel/sdk'; const trackRouter: FastifyPluginCallback = (fastify, opts, done) => { - fastify.addHook( - 'preHandler', - async ( - req: FastifyRequest<{ - Body: TrackHandlerPayload; - }>, - reply, - ) => { - try { - const client = await validateSdkRequest(req.headers).catch((error) => { - if (error instanceof SdkAuthError) { - return reply.status(401).send(error.message); - } - logger.error('Failed to validate sdk request', { error }); - return reply.status(401).send('Unknown validation error'); - }); - - if (!client?.projectId) { - return reply.status(401).send('No project found for this client'); - } - - req.projectId = client.projectId; - req.client = client; - - const bot = req.headers['user-agent'] - ? isBot(req.headers['user-agent']) - : null; - - if (bot) { - if (req.body.type === 'track') { - const path = (req.body.payload.properties?.__path || - req.body.payload.properties?.path) as string | undefined; - await createBotEvent({ - ...bot, - projectId: client.projectId, - path: path ?? '', - createdAt: new Date(), - }); - } - - reply.status(202).send('OK'); - } - } catch (error) { - logger.error('Failed to create bot event', { error }); - reply.status(401).send(); - return; - } - }, - ); + fastify.addHook('preHandler', clientHook); + fastify.addHook('preHandler', isBotHook); fastify.route({ method: 'POST', diff --git a/apps/api/src/utils/auth.ts b/apps/api/src/utils/auth.ts index 0dfb55a8..fc50bae3 100644 --- a/apps/api/src/utils/auth.ts +++ b/apps/api/src/utils/auth.ts @@ -32,9 +32,11 @@ export class SdkAuthError extends Error { } } +type ClientWithProjectId = Client & { projectId: string }; + export async function validateSdkRequest( headers: RawRequestDefaultExpression['headers'], -): Promise { +): Promise { const clientIdNew = headers['openpanel-client-id'] as string; const clientIdOld = headers['mixan-client-id'] as string; const clientSecretNew = headers['openpanel-client-secret'] as string; @@ -57,13 +59,11 @@ export async function validateSdkRequest( throw createError('Ingestion: Missing client id'); } - const client = await db.client - .findUnique({ - where: { - id: clientId, - }, - }) - .catch(() => null); + const client = await db.client.findUnique({ + where: { + id: clientId, + }, + }); if (!client) { throw createError('Ingestion: Invalid client id'); @@ -91,17 +91,17 @@ export async function validateSdkRequest( }); if (domainAllowed) { - return client; + return client as ClientWithProjectId; } if (client.cors === '*' && origin) { - return client; + return client as ClientWithProjectId; } } if (client.secret && clientSecret) { if (await verifyPassword(clientSecret, client.secret)) { - return client; + return client as ClientWithProjectId; } }