This commit is contained in:
Carl-Gerhard Lindesvärd
2024-09-03 21:17:40 +02:00
parent 4cdfe3aed2
commit 8e06bacdb0
13 changed files with 326 additions and 54 deletions

View File

@@ -50,7 +50,6 @@ export default function AddDashboard() {
mutation.mutate({
name,
projectId,
organizationSlug,
});
})}
>

View File

@@ -114,7 +114,6 @@ export default function SaveReport({ report }: SaveReportProps) {
dashboardMutation.mutate({
projectId,
name: value,
organizationSlug,
});
}}
/>

View File

@@ -1,7 +1,7 @@
import { db, getProjectById } from '@openpanel/db';
import { cacheable } from '@openpanel/redis';
export const getProjectAccessCached = cacheable(getProjectAccess, 60 * 60);
export const getProjectAccessCached = cacheable(getProjectAccess, 60 * 5);
export async function getProjectAccess({
userId,
projectId,
@@ -16,14 +16,26 @@ export async function getProjectAccess({
return false;
}
const member = await db.member.findFirst({
where: {
organizationId: project.organizationSlug,
userId,
},
});
const [projectAccess, member] = await Promise.all([
db.projectAccess.findMany({
where: {
userId,
organizationId: project.organizationSlug,
},
}),
db.member.findFirst({
where: {
organizationId: project.organizationSlug,
userId,
},
}),
]);
return member;
if (projectAccess.length === 0 && member) {
return true;
}
return projectAccess.find((item) => item.projectId === projectId);
} catch (err) {
return false;
}
@@ -31,7 +43,7 @@ export async function getProjectAccess({
export const getOrganizationAccessCached = cacheable(
getOrganizationAccess,
60 * 60
60 * 5
);
export async function getOrganizationAccess({
userId,
@@ -47,3 +59,35 @@ export async function getOrganizationAccess({
},
});
}
export const getClientAccessCached = cacheable(getClientAccess, 60 * 5);
export async function getClientAccess({
userId,
clientId,
}: {
userId: string;
clientId: string;
}) {
const client = await db.client.findFirst({
where: {
id: clientId,
},
});
if (!client) {
return false;
}
if (client.projectId) {
return getProjectAccess({ userId, projectId: client.projectId });
}
if (client.organizationId) {
return getOrganizationAccess({
userId,
organizationId: client.organizationId,
});
}
return false;
}

View File

@@ -5,3 +5,9 @@ export const TRPCAccessError = (message: string) =>
code: 'UNAUTHORIZED',
message,
});
export const TRPCNotFoundError = (message: string) =>
new TRPCError({
code: 'NOT_FOUND',
message,
});

View File

@@ -5,6 +5,8 @@ import { hashPassword, stripTrailingSlash } from '@openpanel/common';
import type { Prisma } from '@openpanel/db';
import { db } from '@openpanel/db';
import { getClientAccess } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const clientRouter = createTRPCRouter({
@@ -17,7 +19,16 @@ export const clientRouter = createTRPCRouter({
crossDomain: z.boolean().optional(),
})
)
.mutation(({ input }) => {
.mutation(async ({ input, ctx }) => {
const access = await getClientAccess({
userId: ctx.session.userId,
clientId: input.id,
});
if (!access) {
throw TRPCAccessError('You do not have access to this client');
}
return db.client.update({
where: {
id: input.id,
@@ -66,7 +77,16 @@ export const clientRouter = createTRPCRouter({
id: z.string(),
})
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const access = await getClientAccess({
userId: ctx.session.userId,
clientId: input.id,
});
if (!access) {
throw TRPCAccessError('You do not have access to this client');
}
await db.client.delete({
where: {
id: input.id,

View File

@@ -1,9 +1,16 @@
import { PrismaError } from 'prisma-error-enum';
import { z } from 'zod';
import { db, getDashboardsByProjectId, getId } from '@openpanel/db';
import {
db,
getDashboardsByProjectId,
getId,
getProjectById,
} from '@openpanel/db';
import type { Prisma } from '@openpanel/db';
import { getProjectAccess } from '../access';
import { TRPCAccessError, TRPCNotFoundError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const dashboardRouter = createTRPCRouter({
@@ -13,7 +20,7 @@ export const dashboardRouter = createTRPCRouter({
projectId: z.string(),
})
)
.query(async ({ input }) => {
.query(({ input }) => {
return getDashboardsByProjectId(input.projectId);
}),
create: protectedProcedure
@@ -21,17 +28,31 @@ export const dashboardRouter = createTRPCRouter({
z.object({
name: z.string(),
projectId: z.string(),
organizationSlug: z.string(),
})
)
.mutation(async ({ input: { organizationSlug, projectId, name } }) => {
.mutation(async ({ input, ctx }) => {
const access = await getProjectAccess({
projectId: input.projectId,
userId: ctx.session.userId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
const project = await getProjectById(input.projectId);
if (!project) {
throw TRPCNotFoundError('Project not found');
}
return db.dashboard.create({
data: {
id: await getId('dashboard', name),
projectId: projectId,
organizationSlug: organizationSlug,
organizationId: organizationSlug,
name,
id: await getId('dashboard', input.name),
projectId: input.projectId,
organizationSlug: project.organizationId!,
organizationId: project.organizationId,
name: input.name,
},
});
}),
@@ -42,7 +63,22 @@ export const dashboardRouter = createTRPCRouter({
name: z.string(),
})
)
.mutation(({ input }) => {
.mutation(async ({ input, ctx }) => {
const dashboard = await db.dashboard.findUniqueOrThrow({
where: {
id: input.id,
},
});
const access = await getProjectAccess({
projectId: dashboard.projectId,
userId: ctx.session.userId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this dashboard');
}
return db.dashboard.update({
where: {
id: input.id,
@@ -59,18 +95,33 @@ export const dashboardRouter = createTRPCRouter({
forceDelete: z.boolean().optional(),
})
)
.mutation(async ({ input: { id, forceDelete } }) => {
.mutation(async ({ input, ctx }) => {
const dashboard = await db.dashboard.findUniqueOrThrow({
where: {
id: input.id,
},
});
const access = await getProjectAccess({
projectId: dashboard.projectId,
userId: ctx.session.userId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this dashboard');
}
try {
if (forceDelete) {
if (input.forceDelete) {
await db.report.deleteMany({
where: {
dashboardId: id,
dashboardId: input.id,
},
});
}
await db.dashboard.delete({
where: {
id,
id: input.id,
},
});
} catch (e) {

View File

@@ -27,18 +27,20 @@ export const eventRouter = createTRPCRouter({
conversion: z.boolean().optional(),
})
)
.mutation(({ input: { projectId, name, icon, color, conversion } }) => {
return db.eventMeta.upsert({
where: {
name_projectId: {
name,
projectId,
.mutation(
async ({ input: { projectId, name, icon, color, conversion } }) => {
return db.eventMeta.upsert({
where: {
name_projectId: {
name,
projectId,
},
},
},
create: { projectId, name, icon, color, conversion },
update: { icon, color, conversion },
});
}),
create: { projectId, name, icon, color, conversion },
update: { icon, color, conversion },
});
}
),
byId: protectedProcedure
.input(
@@ -78,8 +80,10 @@ export const eventRouter = createTRPCRouter({
profile: z.boolean().optional(),
})
)
.query(async ({ input }) => getEventList(input)),
conversions: publicProcedure
.query(async ({ input }) => {
return getEventList(input);
}),
conversions: protectedProcedure
.input(
z.object({
projectId: z.string(),

View File

@@ -5,6 +5,8 @@ import { z } from 'zod';
import { db } from '@openpanel/db';
import { zInviteUser } from '@openpanel/validation';
import { getOrganizationAccess, getOrganizationAccessCached } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const organizationRouter = createTRPCRouter({
@@ -15,7 +17,16 @@ export const organizationRouter = createTRPCRouter({
name: z.string(),
})
)
.mutation(({ input }) => {
.mutation(async ({ input, ctx }) => {
const access = await getOrganizationAccess({
userId: ctx.session.userId,
organizationId: input.id,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.organization.update({
where: {
id: input.id,
@@ -29,6 +40,15 @@ export const organizationRouter = createTRPCRouter({
inviteUser: protectedProcedure
.input(zInviteUser)
.mutation(async ({ input, ctx }) => {
const access = await getOrganizationAccess({
userId: ctx.session.userId,
organizationId: input.organizationSlug,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
const email = input.email.toLowerCase();
const userExists = await db.user.findFirst({
where: {
@@ -68,12 +88,22 @@ export const organizationRouter = createTRPCRouter({
memberId: z.string(),
})
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const member = await db.member.findUniqueOrThrow({
where: {
id: input.memberId,
},
});
const access = await getOrganizationAccess({
userId: ctx.session.userId,
organizationId: member.organizationId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
const invitationId = pathOr<string | undefined>(
undefined,
['meta', 'invitationId'],
@@ -107,6 +137,15 @@ export const organizationRouter = createTRPCRouter({
throw new Error('You cannot remove yourself from the organization');
}
const access = await getOrganizationAccess({
userId: ctx.session.userId,
organizationId: input.organizationId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
await db.$transaction([
db.member.deleteMany({
where: {
@@ -131,7 +170,16 @@ export const organizationRouter = createTRPCRouter({
access: z.array(z.string()),
})
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const access = await getOrganizationAccess({
userId: ctx.session.userId,
organizationId: input.organizationSlug,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.$transaction([
db.projectAccess.deleteMany({
where: {

View File

@@ -2,6 +2,8 @@ import { z } from 'zod';
import { db, getId, getProjectsByOrganizationSlug } from '@openpanel/db';
import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const projectRouter = createTRPCRouter({
@@ -23,7 +25,16 @@ export const projectRouter = createTRPCRouter({
name: z.string(),
})
)
.mutation(({ input }) => {
.mutation(async ({ input, ctx }) => {
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: input.id,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.project.update({
where: {
id: input.id,
@@ -56,7 +67,16 @@ export const projectRouter = createTRPCRouter({
id: z.string(),
})
)
.mutation(async ({ input }) => {
.mutation(async ({ input, ctx }) => {
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: input.id,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
await db.project.delete({
where: {
id: input.id,

View File

@@ -3,6 +3,8 @@ import { z } from 'zod';
import { db, getReferences } from '@openpanel/db';
import { zCreateReference, zRange } from '@openpanel/validation';
import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { getChartStartEndDate } from './chart.helpers';
@@ -23,7 +25,22 @@ export const referenceRouter = createTRPCRouter({
),
delete: protectedProcedure
.input(z.object({ id: z.string() }))
.mutation(async ({ input: { id } }) => {
.mutation(async ({ input: { id }, ctx }) => {
const reference = await db.reference.findUniqueOrThrow({
where: {
id,
},
});
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: reference.projectId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.reference.delete({
where: {
id,

View File

@@ -3,6 +3,8 @@ import { z } from 'zod';
import { db } from '@openpanel/db';
import { zReportInput } from '@openpanel/validation';
import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure } from '../trpc';
export const reportRouter = createTRPCRouter({
@@ -13,12 +15,22 @@ export const reportRouter = createTRPCRouter({
dashboardId: z.string(),
})
)
.mutation(async ({ input: { report, dashboardId } }) => {
.mutation(async ({ input: { report, dashboardId }, ctx }) => {
const dashboard = await db.dashboard.findUniqueOrThrow({
where: {
id: dashboardId,
},
});
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: dashboard.projectId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.report.create({
data: {
projectId: dashboard.projectId,
@@ -42,7 +54,22 @@ export const reportRouter = createTRPCRouter({
report: zReportInput.omit({ projectId: true }),
})
)
.mutation(({ input: { report, reportId } }) => {
.mutation(async ({ input: { report, reportId }, ctx }) => {
const dbReport = await db.report.findUniqueOrThrow({
where: {
id: reportId,
},
});
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: dbReport.projectId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.report.update({
where: {
id: reportId,
@@ -66,7 +93,22 @@ export const reportRouter = createTRPCRouter({
reportId: z.string(),
})
)
.mutation(({ input: { reportId } }) => {
.mutation(async ({ input: { reportId }, ctx }) => {
const report = await db.report.findUniqueOrThrow({
where: {
id: reportId,
},
});
const access = await getProjectAccess({
userId: ctx.session.userId,
projectId: report.projectId,
});
if (!access) {
throw TRPCAccessError('You do not have access to this project');
}
return db.report.delete({
where: {
id: reportId,

View File

@@ -10,7 +10,7 @@ const uid = new ShortUniqueId({ length: 6 });
export const shareRouter = createTRPCRouter({
shareOverview: protectedProcedure
.input(zShareOverview)
.mutation(({ input }) => {
.mutation(async ({ input }) => {
return db.shareOverview.upsert({
where: {
projectId: input.projectId,

View File

@@ -5,7 +5,7 @@ import { has } from 'ramda';
import superjson from 'superjson';
import { ZodError } from 'zod';
import { getProjectAccessCached } from './access';
import { getOrganizationAccessCached, getProjectAccessCached } from './access';
import { TRPCAccessError } from './errors';
export function createContext({ req, res }: CreateFastifyContextOptions) {
@@ -45,7 +45,7 @@ const t = initTRPC.context<Context>().create({
},
});
const enforceUserIsAuthed = t.middleware(async ({ ctx, next, input }) => {
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
}
@@ -66,7 +66,7 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next, input }) => {
});
// Only used on protected routes
const enforceProjectAccess = t.middleware(async ({ ctx, next, rawInput }) => {
const enforceAccess = t.middleware(async ({ ctx, next, rawInput }) => {
if (has('projectId', rawInput)) {
const access = await getProjectAccessCached({
userId: ctx.session.userId!,
@@ -78,6 +78,28 @@ const enforceProjectAccess = t.middleware(async ({ ctx, next, rawInput }) => {
}
}
if (has('organizationId', rawInput)) {
const access = await getOrganizationAccessCached({
userId: ctx.session.userId!,
organizationId: rawInput.organizationId as string,
});
if (!access) {
throw TRPCAccessError('You do not have access to this organization');
}
}
if (has('organizationSlug', rawInput)) {
const access = await getOrganizationAccessCached({
userId: ctx.session.userId!,
organizationId: rawInput.organizationSlug as string,
});
if (!access) {
throw TRPCAccessError('You do not have access to this organization');
}
}
return next();
});
@@ -86,4 +108,4 @@ export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure
.use(enforceUserIsAuthed)
.use(enforceProjectAccess);
.use(enforceAccess);