api: add first version of export api

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-04-11 21:30:36 +02:00
committed by Carl-Gerhard Lindesvärd
parent 7ea95afe16
commit 7f8d857508
10 changed files with 232 additions and 7 deletions

View File

@@ -0,0 +1,88 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import type { GetEventListOptions } from '@openpanel/db';
import { ClientType, db, getEventList, getEventsCount } from '@openpanel/db';
type EventsQuery = {
project_id?: string;
event?: string | string[];
start?: string;
end?: string;
page?: string;
};
export async function events(
request: FastifyRequest<{
Querystring: EventsQuery;
}>,
reply: FastifyReply
) {
const query = request.query;
if (query.project_id) {
if (
request.client?.type === ClientType.read &&
request.client?.projectId !== query.project_id
) {
reply.status(403).send({
error: 'Forbidden',
message: 'You do not have access to this project',
});
return;
}
const project = await db.project.findUnique({
where: {
organizationSlug: request.client?.organizationSlug,
id: query.project_id,
},
});
if (!project) {
reply.status(404).send({
error: 'Not Found',
message: 'Project not found',
});
return;
}
}
const projectId = query.project_id ?? request.client?.projectId;
if (!projectId) {
reply.status(400).send({
error: 'Bad Request',
message: 'project_id is required',
});
return;
}
const cursor = (parseInt(query.page || '1', 10) || 1) - 1;
const options: GetEventListOptions = {
projectId,
events: (Array.isArray(query.event) ? query.event : [query.event]).filter(
(s): s is string => typeof s === 'string'
),
startDate: query.start ? new Date(query.start) : undefined,
endDate: query.end ? new Date(query.end) : undefined,
cursor,
take: 50,
meta: false,
profile: true,
};
const [data, totalCount] = await Promise.all([
getEventList(options),
getEventsCount(options),
]);
reply.send({
meta: {
// options,
count: data.length,
totalCount: totalCount,
pages: Math.ceil(totalCount / options.take),
current: cursor + 1,
},
data,
});
}

View File

@@ -1,9 +1,11 @@
import cors from '@fastify/cors';
import Fastify from 'fastify';
import type { IServiceClient } from '@openpanel/db';
import { redisPub } from '@openpanel/redis';
import eventRouter from './routes/event.router';
import exportRouter from './routes/export.router';
import liveRouter from './routes/live.router';
import miscRouter from './routes/misc.router';
import profileRouter from './routes/profile.router';
@@ -12,6 +14,7 @@ import { logger, logInfo } from './utils/logger';
declare module 'fastify' {
interface FastifyRequest {
projectId: string;
client: IServiceClient | null;
}
}
@@ -33,6 +36,7 @@ const startServer = async () => {
fastify.register(profileRouter, { prefix: '/profile' });
fastify.register(liveRouter, { prefix: '/live' });
fastify.register(miscRouter, { prefix: '/misc' });
fastify.register(exportRouter, { prefix: '/export' });
fastify.setErrorHandler((error) => {
fastify.log.error(error);
});

View File

@@ -0,0 +1,37 @@
import * as controller from '@/controllers/export.controller';
import { validateExportRequest } from '@/utils/auth';
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
import { Prisma } from '@openpanel/db';
const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
fastify.addHook('preHandler', async (req: FastifyRequest, reply) => {
try {
const client = await validateExportRequest(req.headers);
req.client = client;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
return reply.status(401).send({
error: 'Unauthorized',
message: 'Client ID seems to be malformed',
});
} else if (e instanceof Error) {
return reply
.status(401)
.send({ error: 'Unauthorized', message: e.message });
}
return reply
.status(401)
.send({ error: 'Unauthorized', message: 'Unexpected error' });
}
});
fastify.route({
method: 'GET',
url: '/events',
handler: controller.events,
});
done();
};
export default eventRouter;

View File

@@ -1,7 +1,8 @@
import type { RawRequestDefaultExpression } from 'fastify';
import { verifyPassword } from '@openpanel/common';
import { db } from '@openpanel/db';
import type { IServiceClient } from '@openpanel/db';
import { ClientType, db } from '@openpanel/db';
import { logger } from './logger';
@@ -89,5 +90,39 @@ export async function validateSdkRequest(
}
}
if (!client.projectId) {
throw new Error('No project id found for client');
}
return client.projectId;
}
export async function validateExportRequest(
headers: RawRequestDefaultExpression['headers']
): Promise<IServiceClient> {
const clientId = headers['openpanel-client-id'] as string;
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
const client = await db.client.findUnique({
where: {
id: clientId,
},
});
if (!client) {
throw new Error('Export: Invalid client id');
}
if (!client.secret) {
throw new Error('Export: Client has no secret');
}
if (client.type === ClientType.write) {
throw new Error('Export: Client is not allowed to export');
}
if (!(await verifyPassword(clientSecret, client.secret))) {
throw new Error('Export: Invalid client secret');
}
return client;
}