add better access control
This commit is contained in:
@@ -29,6 +29,9 @@ ENV NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
|
|||||||
ARG CLERK_SECRET_KEY
|
ARG CLERK_SECRET_KEY
|
||||||
ENV CLERK_SECRET_KEY=$CLERK_SECRET_KEY
|
ENV CLERK_SECRET_KEY=$CLERK_SECRET_KEY
|
||||||
|
|
||||||
|
ARG CLERK_PUBLIC_PEM_KEY
|
||||||
|
ENV CLERK_PUBLIC_PEM_KEY=$CLERK_PUBLIC_PEM_KEY
|
||||||
|
|
||||||
ARG SEVENTY_SEVEN_API_KEY
|
ARG SEVENTY_SEVEN_API_KEY
|
||||||
ENV SEVENTY_SEVEN_API_KEY=$SEVENTY_SEVEN_API_KEY
|
ENV SEVENTY_SEVEN_API_KEY=$SEVENTY_SEVEN_API_KEY
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"fastify": "^4.25.2",
|
"fastify": "^4.25.2",
|
||||||
"fastify-metrics": "^11.0.0",
|
"fastify-metrics": "^11.0.0",
|
||||||
"ico-to-png": "^0.2.1",
|
"ico-to-png": "^0.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"pino": "^8.17.2",
|
"pino": "^8.17.2",
|
||||||
"pino-pretty": "^10.3.1",
|
"pino-pretty": "^10.3.1",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
"@openpanel/prettier-config": "workspace:*",
|
"@openpanel/prettier-config": "workspace:*",
|
||||||
"@openpanel/sdk": "workspace:*",
|
"@openpanel/sdk": "workspace:*",
|
||||||
"@openpanel/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/ramda": "^0.29.6",
|
"@types/ramda": "^0.29.6",
|
||||||
"@types/request-ip": "^0.0.41",
|
"@types/request-ip": "^0.0.41",
|
||||||
"@types/sqlstring": "^2.3.2",
|
"@types/sqlstring": "^2.3.2",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAuth } from '@clerk/fastify';
|
import { validateClerkJwt } from '@/utils/auth';
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { escape } from 'sqlstring';
|
import { escape } from 'sqlstring';
|
||||||
import superjson from 'superjson';
|
import superjson from 'superjson';
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
transformMinimalEvent,
|
transformMinimalEvent,
|
||||||
} from '@openpanel/db';
|
} from '@openpanel/db';
|
||||||
import { redis, redisPub, redisSub } from '@openpanel/redis';
|
import { redis, redisPub, redisSub } from '@openpanel/redis';
|
||||||
|
import { getProjectAccess } from '@openpanel/trpc';
|
||||||
|
|
||||||
export function getLiveEventInfo(key: string) {
|
export function getLiveEventInfo(key: string) {
|
||||||
return key.split(':').slice(2) as [string, string];
|
return key.split(':').slice(2) as [string, string];
|
||||||
@@ -106,7 +107,7 @@ export function wsEvents(connection: { socket: WebSocket }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wsProjectEvents(
|
export async function wsProjectEvents(
|
||||||
connection: {
|
connection: {
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
},
|
},
|
||||||
@@ -114,10 +115,19 @@ export function wsProjectEvents(
|
|||||||
Params: {
|
Params: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
Querystring: {
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
}>
|
}>
|
||||||
) {
|
) {
|
||||||
const { params } = req;
|
const { params, query } = req;
|
||||||
const auth = getAuth(req);
|
const { token } = query;
|
||||||
|
const decoded = validateClerkJwt(token);
|
||||||
|
const userId = decoded?.sub;
|
||||||
|
const access = await getProjectAccess({
|
||||||
|
userId: userId!,
|
||||||
|
projectId: params.projectId,
|
||||||
|
});
|
||||||
|
|
||||||
redisSub.subscribe('event');
|
redisSub.subscribe('event');
|
||||||
|
|
||||||
@@ -127,7 +137,7 @@ export function wsProjectEvents(
|
|||||||
const profile = await getProfileById(event.profileId, event.projectId);
|
const profile = await getProfileById(event.profileId, event.projectId);
|
||||||
connection.socket.send(
|
connection.socket.send(
|
||||||
superjson.stringify(
|
superjson.stringify(
|
||||||
auth.userId
|
access
|
||||||
? {
|
? {
|
||||||
...event,
|
...event,
|
||||||
profile,
|
profile,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RawRequestDefaultExpression } from 'fastify';
|
import type { RawRequestDefaultExpression } from 'fastify';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
import { verifyPassword } from '@openpanel/common';
|
import { verifyPassword } from '@openpanel/common';
|
||||||
import type { IServiceClient } from '@openpanel/db';
|
import type { IServiceClient } from '@openpanel/db';
|
||||||
@@ -103,3 +104,23 @@ export async function validateExportRequest(
|
|||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateClerkJwt(token?: string) {
|
||||||
|
if (!token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(
|
||||||
|
token,
|
||||||
|
process.env.CLERK_PUBLIC_PEM_KEY!.replace(/\\n/g, '\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof decoded === 'object') {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,34 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { use, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useAuth } from '@clerk/nextjs';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import useWebSocket from 'react-use-websocket';
|
||||||
|
|
||||||
import { getSuperJson } from '@openpanel/common';
|
import { getSuperJson } from '@openpanel/common';
|
||||||
|
|
||||||
export default function useWS<T>(path: string, onMessage: (event: T) => void) {
|
export default function useWS<T>(path: string, onMessage: (event: T) => void) {
|
||||||
|
const auth = useAuth();
|
||||||
const ws = String(process.env.NEXT_PUBLIC_API_URL)
|
const ws = String(process.env.NEXT_PUBLIC_API_URL)
|
||||||
.replace(/^https/, 'wss')
|
.replace(/^https/, 'wss')
|
||||||
.replace(/^http/, 'ws');
|
.replace(/^http/, 'ws');
|
||||||
const [socketUrl, setSocketUrl] = useState(`${ws}${path}`);
|
const [baseUrl, setBaseUrl] = useState(`${ws}${path}`);
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const socketUrl = useMemo(
|
||||||
|
() => (token ? `${baseUrl}?token=${token}` : baseUrl),
|
||||||
|
[baseUrl, token]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (socketUrl === `${ws}${path}`) return;
|
if (auth.isSignedIn) {
|
||||||
setSocketUrl(`${ws}${path}`);
|
auth.getToken().then(setToken);
|
||||||
}, [path, socketUrl, ws]);
|
}
|
||||||
|
}, [auth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (baseUrl === `${ws}${path}`) return;
|
||||||
|
setBaseUrl(`${ws}${path}`);
|
||||||
|
}, [path, baseUrl, ws]);
|
||||||
|
console.log('socketUrl', socketUrl);
|
||||||
|
|
||||||
useWebSocket(socketUrl, {
|
useWebSocket(socketUrl, {
|
||||||
shouldReconnect: () => true,
|
shouldReconnect: () => true,
|
||||||
|
|||||||
17
packages/redis/cachable.ts
Normal file
17
packages/redis/cachable.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { redis } from './redis';
|
||||||
|
|
||||||
|
export function cacheable<T extends (...args: any) => any>(
|
||||||
|
fn: T,
|
||||||
|
expire: number
|
||||||
|
) {
|
||||||
|
return async function (...args: Parameters<T>): Promise<ReturnType<T>> {
|
||||||
|
const key = `cachable:${fn.name}:${JSON.stringify(args)}`;
|
||||||
|
const cached = await redis.get(key);
|
||||||
|
if (cached) {
|
||||||
|
return JSON.parse(cached);
|
||||||
|
}
|
||||||
|
const result = await fn(...(args as any));
|
||||||
|
redis.setex(key, expire, JSON.stringify(result));
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,2 @@
|
|||||||
import type { RedisOptions } from 'ioredis';
|
export * from './redis';
|
||||||
import Redis from 'ioredis';
|
export * from './cachable';
|
||||||
|
|
||||||
const options: RedisOptions = {
|
|
||||||
connectTimeout: 10000,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const redis = new Redis(process.env.REDIS_URL!, options);
|
|
||||||
export const redisSub = new Redis(process.env.REDIS_URL!, options);
|
|
||||||
export const redisPub = new Redis(process.env.REDIS_URL!, options);
|
|
||||||
|
|||||||
10
packages/redis/redis.ts
Normal file
10
packages/redis/redis.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { RedisOptions } from 'ioredis';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
const options: RedisOptions = {
|
||||||
|
connectTimeout: 10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const redis = new Redis(process.env.REDIS_URL!, options);
|
||||||
|
export const redisSub = new Redis(process.env.REDIS_URL!, options);
|
||||||
|
export const redisPub = new Redis(process.env.REDIS_URL!, options);
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './src/root';
|
export * from './src/root';
|
||||||
export * from './src/trpc';
|
export * from './src/trpc';
|
||||||
|
export { getProjectAccess } from './src/access';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@openpanel/constants": "workspace:*",
|
"@openpanel/constants": "workspace:*",
|
||||||
"@openpanel/db": "workspace:*",
|
"@openpanel/db": "workspace:*",
|
||||||
"@openpanel/validation": "workspace:*",
|
"@openpanel/validation": "workspace:*",
|
||||||
|
"@openpanel/redis": "workspace:*",
|
||||||
"@seventy-seven/sdk": "0.0.0-beta.2",
|
"@seventy-seven/sdk": "0.0.0-beta.2",
|
||||||
"@trpc/server": "^10.45.1",
|
"@trpc/server": "^10.45.1",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
@@ -44,4 +45,4 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": "@openpanel/prettier-config"
|
"prettier": "@openpanel/prettier-config"
|
||||||
}
|
}
|
||||||
53
packages/trpc/src/access.ts
Normal file
53
packages/trpc/src/access.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { clerkClient } from '@clerk/fastify';
|
||||||
|
|
||||||
|
import { getProjectById } from '@openpanel/db';
|
||||||
|
import { cacheable } from '@openpanel/redis';
|
||||||
|
|
||||||
|
export const getProjectAccessCached = cacheable(getProjectAccess, 60 * 60);
|
||||||
|
export async function getProjectAccess({
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
projectId: string;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
// Check if user has access to the project
|
||||||
|
const [project, organizations] = await Promise.all([
|
||||||
|
getProjectById(projectId),
|
||||||
|
clerkClient.users.getOrganizationMembershipList({
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!organizations.data.find(
|
||||||
|
(org) => org.organization.slug === project.organizationSlug
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOrganizationAccessCached = cacheable(
|
||||||
|
getOrganizationAccess,
|
||||||
|
60 * 60
|
||||||
|
);
|
||||||
|
export async function getOrganizationAccess({
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
organizationId: string;
|
||||||
|
}) {
|
||||||
|
const organizations = await clerkClient.users.getOrganizationMembershipList({
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!organizations.data.find(
|
||||||
|
(org) => org.organization.id === organizationId
|
||||||
|
);
|
||||||
|
}
|
||||||
7
packages/trpc/src/errors.ts
Normal file
7
packages/trpc/src/errors.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
|
export const TRPCAccessError = (message: string) =>
|
||||||
|
new TRPCError({
|
||||||
|
code: 'UNAUTHORIZED',
|
||||||
|
message,
|
||||||
|
});
|
||||||
@@ -3,10 +3,12 @@ import { escape } from 'sqlstring';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { average, max, min, round, slug, sum } from '@openpanel/common';
|
import { average, max, min, round, slug, sum } from '@openpanel/common';
|
||||||
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
import { chQuery, createSqlBuilder, db } from '@openpanel/db';
|
||||||
import { zChartInput } from '@openpanel/validation';
|
import { zChartInput } from '@openpanel/validation';
|
||||||
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
|
import { getProjectAccessCached } from '../access';
|
||||||
|
import { TRPCAccessError } from '../errors';
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||||
import {
|
import {
|
||||||
getChartPrevStartEndDate,
|
getChartPrevStartEndDate,
|
||||||
@@ -111,8 +113,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
)(properties);
|
)(properties);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// TODO: Make this private
|
values: protectedProcedure
|
||||||
values: publicProcedure
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
event: z.string(),
|
event: z.string(),
|
||||||
@@ -154,7 +155,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
funnel: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
funnel: protectedProcedure.input(zChartInput).query(async ({ input }) => {
|
||||||
const currentPeriod = getChartStartEndDate(input);
|
const currentPeriod = getChartStartEndDate(input);
|
||||||
const previousPeriod = getChartPrevStartEndDate({
|
const previousPeriod = getChartPrevStartEndDate({
|
||||||
range: input.range,
|
range: input.range,
|
||||||
@@ -172,7 +173,7 @@ export const chartRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
funnelStep: publicProcedure
|
funnelStep: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
zChartInput.extend({
|
zChartInput.extend({
|
||||||
step: z.number(),
|
step: z.number(),
|
||||||
@@ -183,8 +184,27 @@ export const chartRouter = createTRPCRouter({
|
|||||||
return getFunnelStep({ ...input, ...currentPeriod });
|
return getFunnelStep({ ...input, ...currentPeriod });
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// TODO: Make this private
|
chart: publicProcedure.input(zChartInput).query(async ({ input, ctx }) => {
|
||||||
chart: publicProcedure.input(zChartInput).query(async ({ input }) => {
|
if (ctx.session.userId) {
|
||||||
|
const access = await getProjectAccessCached({
|
||||||
|
projectId: input.projectId,
|
||||||
|
userId: ctx.session.userId,
|
||||||
|
});
|
||||||
|
if (!access) {
|
||||||
|
throw TRPCAccessError('You do not have access to this project');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const share = await db.shareOverview.findFirst({
|
||||||
|
where: {
|
||||||
|
projectId: input.projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!share) {
|
||||||
|
throw TRPCAccessError('You do not have access to this project');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const currentPeriod = getChartStartEndDate(input);
|
const currentPeriod = getChartStartEndDate(input);
|
||||||
const previousPeriod = getChartPrevStartEndDate({
|
const previousPeriod = getChartPrevStartEndDate({
|
||||||
range: input.range,
|
range: input.range,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { chQuery, convertClickhouseDateToJs, db } from '@openpanel/db';
|
import { chQuery, convertClickhouseDateToJs, db } from '@openpanel/db';
|
||||||
|
|
||||||
|
import { getProjectAccessCached } from '../access';
|
||||||
|
import { TRPCAccessError } from '../errors';
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
export const eventRouter = createTRPCRouter({
|
export const eventRouter = createTRPCRouter({
|
||||||
@@ -37,7 +39,27 @@ export const eventRouter = createTRPCRouter({
|
|||||||
limit: z.number().default(8),
|
limit: z.number().default(8),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ input: { projectId, cursor, limit } }) => {
|
.query(async ({ input: { projectId, cursor, limit }, ctx }) => {
|
||||||
|
if (ctx.session.userId) {
|
||||||
|
const access = await getProjectAccessCached({
|
||||||
|
projectId,
|
||||||
|
userId: ctx.session.userId,
|
||||||
|
});
|
||||||
|
if (!access) {
|
||||||
|
throw TRPCAccessError('You do not have access to this project');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const share = await db.shareOverview.findFirst({
|
||||||
|
where: {
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!share) {
|
||||||
|
throw TRPCAccessError('You do not have access to this project');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [events, counts] = await Promise.all([
|
const [events, counts] = await Promise.all([
|
||||||
chQuery<{
|
chQuery<{
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
import { chQuery, createSqlBuilder } from '@openpanel/db';
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||||
|
|
||||||
export const profileRouter = createTRPCRouter({
|
export const profileRouter = createTRPCRouter({
|
||||||
properties: protectedProcedure
|
properties: protectedProcedure
|
||||||
@@ -28,7 +28,7 @@ export const profileRouter = createTRPCRouter({
|
|||||||
)(properties);
|
)(properties);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
values: publicProcedure
|
values: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
property: z.string(),
|
property: z.string(),
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { getAuth } from '@clerk/fastify';
|
import { getAuth } from '@clerk/fastify';
|
||||||
import { initTRPC, TRPCError } from '@trpc/server';
|
import { initTRPC, TRPCError } from '@trpc/server';
|
||||||
import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
|
import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify';
|
||||||
|
import { has } from 'ramda';
|
||||||
import superjson from 'superjson';
|
import superjson from 'superjson';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
|
|
||||||
|
import { getProjectAccessCached } from './access';
|
||||||
|
import { TRPCAccessError } from './errors';
|
||||||
|
|
||||||
export function createContext({ req, res }: CreateFastifyContextOptions) {
|
export function createContext({ req, res }: CreateFastifyContextOptions) {
|
||||||
return {
|
return {
|
||||||
req,
|
req,
|
||||||
@@ -41,10 +45,11 @@ const t = initTRPC.context<Context>().create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(async ({ ctx, next, input }) => {
|
||||||
if (!ctx.session?.userId) {
|
if (!ctx.session?.userId) {
|
||||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
|
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
@@ -60,7 +65,25 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Only used on protected routes
|
||||||
|
const enforceProjectAccess = t.middleware(async ({ ctx, next, rawInput }) => {
|
||||||
|
if (has('projectId', rawInput)) {
|
||||||
|
const access = await getProjectAccessCached({
|
||||||
|
userId: ctx.session.userId!,
|
||||||
|
projectId: rawInput.projectId as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!access) {
|
||||||
|
throw TRPCAccessError('You do not have access to this project');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
export const createTRPCRouter = t.router;
|
export const createTRPCRouter = t.router;
|
||||||
|
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
|
export const protectedProcedure = t.procedure
|
||||||
|
.use(enforceUserIsAuthed)
|
||||||
|
.use(enforceProjectAccess);
|
||||||
|
|||||||
81
pnpm-lock.yaml
generated
81
pnpm-lock.yaml
generated
@@ -71,6 +71,9 @@ importers:
|
|||||||
ico-to-png:
|
ico-to-png:
|
||||||
specifier: ^0.2.1
|
specifier: ^0.2.1
|
||||||
version: 0.2.1
|
version: 0.2.1
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.2
|
||||||
|
version: 9.0.2
|
||||||
pino:
|
pino:
|
||||||
specifier: ^8.17.2
|
specifier: ^8.17.2
|
||||||
version: 8.19.0
|
version: 8.19.0
|
||||||
@@ -111,6 +114,9 @@ importers:
|
|||||||
'@openpanel/tsconfig':
|
'@openpanel/tsconfig':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../tooling/typescript
|
version: link:../../tooling/typescript
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.6
|
||||||
|
version: 9.0.6
|
||||||
'@types/ramda':
|
'@types/ramda':
|
||||||
specifier: ^0.29.6
|
specifier: ^0.29.6
|
||||||
version: 0.29.10
|
version: 0.29.10
|
||||||
@@ -1243,6 +1249,9 @@ importers:
|
|||||||
'@openpanel/db':
|
'@openpanel/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../db
|
version: link:../db
|
||||||
|
'@openpanel/redis':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../redis
|
||||||
'@openpanel/validation':
|
'@openpanel/validation':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../validation
|
version: link:../validation
|
||||||
@@ -7531,6 +7540,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/jsonwebtoken@9.0.6:
|
||||||
|
resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 18.19.17
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/katex@0.16.7:
|
/@types/katex@0.16.7:
|
||||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -8618,6 +8633,10 @@ packages:
|
|||||||
buffer-fill: 1.0.0
|
buffer-fill: 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/buffer-fill@1.0.0:
|
/buffer-fill@1.0.0:
|
||||||
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
|
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -10012,6 +10031,12 @@ packages:
|
|||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
|
/ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ee-first@1.1.1:
|
/ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -12832,6 +12857,22 @@ packages:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/jsonwebtoken@9.0.2:
|
||||||
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
jws: 3.2.2
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.6.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jsx-ast-utils@3.3.5:
|
/jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -12842,6 +12883,21 @@ packages:
|
|||||||
object.values: 1.1.7
|
object.values: 1.1.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/jwa@1.4.1:
|
||||||
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/jws@3.2.2:
|
||||||
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
|
dependencies:
|
||||||
|
jwa: 1.4.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/katex@0.16.9:
|
/katex@0.16.9:
|
||||||
resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==}
|
resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -13073,17 +13129,40 @@ packages:
|
|||||||
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.isarguments@3.1.0:
|
/lodash.isarguments@3.1.0:
|
||||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.isplainobject@4.0.6:
|
/lodash.isplainobject@4.0.6:
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
dev: true
|
|
||||||
|
/lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.merge@4.6.2:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
|
/lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.sortby@4.7.0:
|
/lodash.sortby@4.7.0:
|
||||||
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Reference in New Issue
Block a user