wip
This commit is contained in:
@@ -50,7 +50,6 @@ export default function AddDashboard() {
|
||||
mutation.mutate({
|
||||
name,
|
||||
projectId,
|
||||
organizationSlug,
|
||||
});
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -114,7 +114,6 @@ export default function SaveReport({ report }: SaveReportProps) {
|
||||
dashboardMutation.mutate({
|
||||
projectId,
|
||||
name: value,
|
||||
organizationSlug,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,3 +5,9 @@ export const TRPCAccessError = (message: string) =>
|
||||
code: 'UNAUTHORIZED',
|
||||
message,
|
||||
});
|
||||
|
||||
export const TRPCNotFoundError = (message: string) =>
|
||||
new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user