feature(dashboard): add ability to filter out events by profile id and ip (#101)

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-12-07 21:34:32 +01:00
committed by GitHub
parent 27ee623584
commit f4ad97d87d
39 changed files with 1148 additions and 542 deletions

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "projects" ADD COLUMN "cors" TEXT,
ADD COLUMN "crossDomain" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "domain" TEXT,
ADD COLUMN "filters" JSONB NOT NULL DEFAULT '[]';

View File

@@ -0,0 +1,9 @@
/*
Warnings:
- The `cors` column on the `projects` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterTable
ALTER TABLE "projects" DROP COLUMN "cors",
ADD COLUMN "cors" TEXT[] DEFAULT ARRAY[]::TEXT[];

View File

@@ -80,6 +80,11 @@ model Project {
organizationId String
eventsCount Int @default(0)
types ProjectType[] @default([])
domain String?
cors String[] @default([])
crossDomain Boolean @default(false)
/// [IPrismaProjectFilters]
filters Json @default("[]")
events Event[]
profiles Profile[]

View File

@@ -39,7 +39,7 @@ export const CLICKHOUSE_OPTIONS: NodeClickHouseClientConfigOptions = {
export const originalCh = createClient({
// TODO: remove this after migration
url: process.env.CLICKHOUSE_URL_CLUSTER ?? process.env.CLICKHOUSE_URL,
url: process.env.CLICKHOUSE_URL_DIRECT ?? process.env.CLICKHOUSE_URL,
...CLICKHOUSE_OPTIONS,
});

View File

@@ -1,3 +1,4 @@
import { cacheable } from '@openpanel/redis';
import type { Client, Prisma } from '../prisma-client';
import { db } from '../prisma-client';
@@ -21,3 +22,16 @@ export async function getClientsByOrganizationId(organizationId: string) {
},
});
}
export async function getClientById(
id: string,
): Promise<IServiceClientWithProject | null> {
return db.client.findUnique({
where: { id },
include: {
project: true,
},
});
}
export const getClientByIdCached = cacheable(getClientById, 60 * 60 * 24);

View File

@@ -1,6 +1,7 @@
import type {
IIntegrationConfig,
INotificationRuleConfig,
IProjectFilters,
} from '@openpanel/validation';
import type { INotificationPayload } from './services/notification.service';
@@ -9,5 +10,6 @@ declare global {
type IPrismaNotificationRuleConfig = INotificationRuleConfig;
type IPrismaIntegrationConfig = IIntegrationConfig;
type IPrismaNotificationPayload = INotificationPayload;
type IPrismaProjectFilters = IProjectFilters[];
}
}

View File

@@ -1,7 +1,6 @@
import crypto from 'node:crypto';
import { z } from 'zod';
import { stripTrailingSlash } from '@openpanel/common';
import type { Prisma } from '@openpanel/db';
import { db } from '@openpanel/db';
@@ -47,8 +46,6 @@ export const clientRouter = createTRPCRouter({
name: z.string(),
projectId: z.string(),
organizationId: z.string(),
cors: z.string().nullable(),
crossDomain: z.boolean().optional(),
type: z.enum(['read', 'write', 'root']).optional(),
}),
)
@@ -59,9 +56,7 @@ export const clientRouter = createTRPCRouter({
projectId: input.projectId,
name: input.name,
type: input.type ?? 'write',
cors: input.cors ? stripTrailingSlash(input.cors) : null,
secret: await hashPassword(secret),
crossDomain: input.crossDomain ?? false,
};
const client = await db.client.create({ data });

View File

@@ -2,11 +2,14 @@ import { z } from 'zod';
import {
db,
getClientById,
getClientByIdCached,
getId,
getProjectByIdCached,
getProjectsByOrganizationId,
} from '@openpanel/db';
import { zProject } from '@openpanel/validation';
import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
@@ -24,13 +27,12 @@ export const projectRouter = createTRPCRouter({
}),
update: protectedProcedure
.input(
z.object({
id: z.string(),
name: z.string(),
}),
)
.input(zProject.partial())
.mutation(async ({ input, ctx }) => {
if (!input.id) {
throw new Error('Project ID is required to update a project');
}
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: input.id,
@@ -39,30 +41,52 @@ export const projectRouter = createTRPCRouter({
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
const res = await db.project.update({
where: {
id: input.id,
},
data: {
name: input.name,
crossDomain: input.crossDomain,
filters: input.filters,
cors: input.cors,
domain: input.domain,
},
include: {
clients: {
select: {
id: true,
},
},
},
});
await getProjectByIdCached.clear(input.id);
await Promise.all([
getProjectByIdCached.clear(input.id),
res.clients.map((client) => {
getClientByIdCached.clear(client.id);
}),
]);
return res;
}),
create: protectedProcedure
.input(
z.object({
name: z.string().min(1),
organizationId: z.string(),
}),
zProject.omit({ id: true }).merge(
z.object({
organizationId: z.string(),
}),
),
)
.mutation(async ({ input: { name, organizationId } }) => {
.mutation(async ({ input }) => {
return db.project.create({
data: {
id: await getId('project', name),
organizationId,
name: name,
id: await getId('project', input.name),
organizationId: input.organizationId,
name: input.name,
domain: input.domain,
cors: input.cors,
crossDomain: input.crossDomain,
filters: [],
},
});
}),

View File

@@ -271,3 +271,31 @@ export const zCreateNotificationRule = z.object({
sendToEmail: z.boolean(),
projectId: z.string(),
});
export const zProjectFilterIp = z.object({
type: z.literal('ip'),
ip: z.string(),
});
export type IProjectFilterIp = z.infer<typeof zProjectFilterIp>;
export const zProjectFilterProfileId = z.object({
type: z.literal('profile_id'),
profileId: z.string(),
});
export type IProjectFilterProfileId = z.infer<typeof zProjectFilterProfileId>;
export const zProjectFilters = z.discriminatedUnion('type', [
zProjectFilterIp,
zProjectFilterProfileId,
]);
export type IProjectFilters = z.infer<typeof zProjectFilters>;
export const zProject = z.object({
id: z.string(),
name: z.string().min(1),
filters: z.array(zProjectFilters).default([]),
domain: z.string().url().or(z.literal('').or(z.null())),
cors: z.array(z.string()).default([]),
crossDomain: z.boolean().default(false),
});
export type IProjectEdit = z.infer<typeof zProject>;