feat: add trpc framework
This commit is contained in:
parent
53c0fd563d
commit
b9b5ee5ae1
@ -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": {
|
||||
|
@ -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'}
|
||||
|
@ -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,6 +48,7 @@ AppRoutes.displayName = 'AppRoutes';
|
||||
export const App: React.FC = React.memo(() => {
|
||||
return (
|
||||
<div className="App">
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<TokenLoginContainer>
|
||||
@ -54,6 +56,7 @@ export const App: React.FC = React.memo(() => {
|
||||
</TokenLoginContainer>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
19
src/client/api/trpc.ts
Normal file
19
src/client/api/trpc.ts
Normal file
@ -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<AppRouter>();
|
||||
|
||||
export const trpcClient = trpc.createClient({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: '/trpc',
|
||||
async headers() {
|
||||
return {
|
||||
Authorization: `Bearer ${getJWT()}`,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
@ -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 });
|
||||
|
16
src/server/trpc/index.ts
Normal file
16
src/server/trpc/index.ts
Normal file
@ -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,
|
||||
});
|
90
src/server/trpc/trpc.ts
Normal file
90
src/server/trpc/trpc.ts
Normal file
@ -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<typeof createContext>;
|
||||
const t = initTRPC.context<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();
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user