diff --git a/package.json b/package.json
index 2809dd6..674c356 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,9 @@
"@ant-design/icons": "^5.2.5",
"@prisma/client": "^5.2.0",
"@tanstack/react-query": "^4.33.0",
+ "@trpc/client": "^10.38.4",
+ "@trpc/react-query": "^10.38.4",
+ "@trpc/server": "^10.38.4",
"@types/uuid": "^9.0.3",
"antd": "^5.8.5",
"axios": "^1.5.0",
@@ -53,6 +56,7 @@
"uuid": "^9.0.0",
"vite-express": "^0.10.0",
"yup": "^1.2.0",
+ "zod": "^3.22.2",
"zustand": "^4.4.1"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 40a182d..5f53aa1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,6 +13,15 @@ dependencies:
'@tanstack/react-query':
specifier: ^4.33.0
version: 4.33.0(react-dom@18.2.0)(react@18.2.0)
+ '@trpc/client':
+ specifier: ^10.38.4
+ version: 10.38.4(@trpc/server@10.38.4)
+ '@trpc/react-query':
+ specifier: ^10.38.4
+ version: 10.38.4(@tanstack/react-query@4.33.0)(@trpc/client@10.38.4)(@trpc/server@10.38.4)(react-dom@18.2.0)(react@18.2.0)
+ '@trpc/server':
+ specifier: ^10.38.4
+ version: 10.38.4
'@types/uuid':
specifier: ^9.0.3
version: 9.0.3
@@ -121,6 +130,9 @@ dependencies:
yup:
specifier: ^1.2.0
version: 1.2.0
+ zod:
+ specifier: ^3.22.2
+ version: 3.22.2
zustand:
specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.21)(react@18.2.0)
@@ -1803,6 +1815,34 @@ packages:
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
+ /@trpc/client@10.38.4(@trpc/server@10.38.4):
+ resolution: {integrity: sha512-svpZ9Iq9cnn+XfXQZF8PMt1YxAtNYeGiKZ/pGpcume7RqJrra/kWwU41gbax8d/cAg3YKUgNft9dZFKMYtXuYw==}
+ peerDependencies:
+ '@trpc/server': 10.38.4
+ dependencies:
+ '@trpc/server': 10.38.4
+ dev: false
+
+ /@trpc/react-query@10.38.4(@tanstack/react-query@4.33.0)(@trpc/client@10.38.4)(@trpc/server@10.38.4)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-wEreUn9E+ZMKn/oRWlhHzmSgG5SG9WpmE1F27PPjn3I0S92aYRQvVsmV43v2OAL1VUYP2aHLk7gXx2luKLLRyw==}
+ peerDependencies:
+ '@tanstack/react-query': ^4.18.0
+ '@trpc/client': 10.38.4
+ '@trpc/server': 10.38.4
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+ dependencies:
+ '@tanstack/react-query': 4.33.0(react-dom@18.2.0)(react@18.2.0)
+ '@trpc/client': 10.38.4(@trpc/server@10.38.4)
+ '@trpc/server': 10.38.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
+ /@trpc/server@10.38.4:
+ resolution: {integrity: sha512-xSMTwnKA/Unxu5fbAkQ7cApHeWj1rTEA3XgrGDcPHn03fmeIidIKxronM46N46ZF4CCexey4JWzu89XxA16uIA==}
+ dev: false
+
/@tsconfig/node10@1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@@ -6145,6 +6185,10 @@ packages:
type-fest: 2.19.0
dev: false
+ /zod@3.22.2:
+ resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==}
+ dev: false
+
/zustand@4.4.1(@types/react@18.2.21)(react@18.2.0):
resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
engines: {node: '>=12.7.0'}
diff --git a/src/client/App.tsx b/src/client/App.tsx
index b987976..0d879f5 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -12,6 +12,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './api/cache';
import { TokenLoginContainer } from './components/TokenLoginContainer';
import React from 'react';
+import { trpc, trpcClient } from './api/trpc';
export const AppRoutes: React.FC = React.memo(() => {
const { info } = useUserStore();
@@ -47,13 +48,15 @@ AppRoutes.displayName = 'AppRoutes';
export const App: React.FC = React.memo(() => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
});
diff --git a/src/client/api/trpc.ts b/src/client/api/trpc.ts
new file mode 100644
index 0000000..683202d
--- /dev/null
+++ b/src/client/api/trpc.ts
@@ -0,0 +1,19 @@
+import { createTRPCReact } from '@trpc/react-query';
+import type { AppRouter } from '../../server/trpc';
+import { httpBatchLink } from '@trpc/client';
+import { getJWT } from './auth';
+
+export const trpc = createTRPCReact();
+
+export const trpcClient = trpc.createClient({
+ links: [
+ httpBatchLink({
+ url: '/trpc',
+ async headers() {
+ return {
+ Authorization: `Bearer ${getJWT()}`,
+ };
+ },
+ }),
+ ],
+});
diff --git a/src/server/main.ts b/src/server/main.ts
index f0295a9..875b465 100644
--- a/src/server/main.ts
+++ b/src/server/main.ts
@@ -11,6 +11,7 @@ import { websiteRouter } from './router/website';
import { workspaceRouter } from './router/workspace';
import { telemetryRouter } from './router/telemetry';
import { initSocketio } from './ws';
+import { trpcExpressMiddleware } from './trpc';
const port = Number(process.env.PORT || 12345);
@@ -31,6 +32,8 @@ app.use('/api/website', websiteRouter);
app.use('/api/workspace', workspaceRouter);
app.use('/api/telemetry', telemetryRouter);
+app.use('/trpc', trpcExpressMiddleware);
+
app.use((err: any, req: any, res: any, next: any) => {
console.error(err);
res.status(500).json({ message: err.message });
diff --git a/src/server/trpc/index.ts b/src/server/trpc/index.ts
new file mode 100644
index 0000000..6d32a5a
--- /dev/null
+++ b/src/server/trpc/index.ts
@@ -0,0 +1,16 @@
+import * as trpcExpress from '@trpc/server/adapters/express';
+import { createContext, publicProcedure, router } from './trpc';
+import { z } from 'zod';
+
+const appRouter = router({
+ debug: publicProcedure.input(z.string()).query((opts) => {
+ return { id: opts.input, name: 'Bilbo' };
+ }),
+});
+
+export type AppRouter = typeof appRouter;
+
+export const trpcExpressMiddleware = trpcExpress.createExpressMiddleware({
+ router: appRouter,
+ createContext,
+});
diff --git a/src/server/trpc/trpc.ts b/src/server/trpc/trpc.ts
new file mode 100644
index 0000000..feb4433
--- /dev/null
+++ b/src/server/trpc/trpc.ts
@@ -0,0 +1,90 @@
+import { initTRPC, inferAsyncReturnType, TRPCError } from '@trpc/server';
+import * as trpcExpress from '@trpc/server/adapters/express';
+import { jwtVerify } from '../middleware/auth';
+import { getWorkspaceUser } from '../model/workspace';
+import { ROLES, SYSTEM_ROLES } from '../utils/const';
+
+export function createContext({
+ req,
+ res,
+}: trpcExpress.CreateExpressContextOptions) {
+ const authorization = req.headers['authorization'] ?? '';
+ const token = authorization.replace('Bearer ', '');
+
+ try {
+ const user = jwtVerify(token);
+
+ return { user };
+ } catch (err) {
+ throw new TRPCError({ code: 'UNAUTHORIZED' });
+ }
+}
+
+type Context = inferAsyncReturnType;
+const t = initTRPC.context().create();
+
+export const middleware = t.middleware;
+export const router = t.router;
+export const publicProcedure = t.procedure;
+
+const isSystemAdmin = middleware(async (opts) => {
+ const { ctx, input } = opts;
+ if (ctx.user.role !== SYSTEM_ROLES.admin) {
+ throw new TRPCError({ code: 'FORBIDDEN' });
+ }
+
+ return opts.next();
+});
+
+export const systemAdminProcedure = t.procedure.use(isSystemAdmin);
+export const workspaceProcedure = t.procedure.use(
+ createWorkspacePermissionMiddleware()
+);
+export const ownerProcedure = t.procedure.use(
+ createWorkspacePermissionMiddleware([ROLES.owner])
+);
+
+/**
+ * Create a trpc middleware which help user check workspace permission
+ */
+function createWorkspacePermissionMiddleware(roles: ROLES[] = []) {
+ return middleware(async (opts) => {
+ const { ctx, input } = opts;
+
+ const workspaceId = _.get(input, 'workspaceId', '');
+ if (!workspaceId) {
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'Payload required workspaceId',
+ });
+ }
+
+ const userId = ctx.user.id;
+
+ if (!userId) {
+ throw new TRPCError({
+ code: 'INTERNAL_SERVER_ERROR',
+ message: 'ctx miss userId',
+ });
+ }
+
+ const info = await getWorkspaceUser(workspaceId, userId);
+ if (!info) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: 'Is not workspace user',
+ });
+ }
+
+ if (Array.isArray(roles) && roles.length > 0) {
+ if (!roles.includes(info.role as ROLES)) {
+ throw new TRPCError({
+ code: 'FORBIDDEN',
+ message: `Workspace roles not has this permission, need ${roles}`,
+ });
+ }
+ }
+
+ return opts.next();
+ });
+}