feat: add trpc framework

This commit is contained in:
moonrailgun 2023-09-27 17:56:32 +08:00
parent 53c0fd563d
commit b9b5ee5ae1
7 changed files with 186 additions and 7 deletions

View File

@ -17,6 +17,9 @@
"@ant-design/icons": "^5.2.5", "@ant-design/icons": "^5.2.5",
"@prisma/client": "^5.2.0", "@prisma/client": "^5.2.0",
"@tanstack/react-query": "^4.33.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", "@types/uuid": "^9.0.3",
"antd": "^5.8.5", "antd": "^5.8.5",
"axios": "^1.5.0", "axios": "^1.5.0",
@ -53,6 +56,7 @@
"uuid": "^9.0.0", "uuid": "^9.0.0",
"vite-express": "^0.10.0", "vite-express": "^0.10.0",
"yup": "^1.2.0", "yup": "^1.2.0",
"zod": "^3.22.2",
"zustand": "^4.4.1" "zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -13,6 +13,15 @@ dependencies:
'@tanstack/react-query': '@tanstack/react-query':
specifier: ^4.33.0 specifier: ^4.33.0
version: 4.33.0(react-dom@18.2.0)(react@18.2.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': '@types/uuid':
specifier: ^9.0.3 specifier: ^9.0.3
version: 9.0.3 version: 9.0.3
@ -121,6 +130,9 @@ dependencies:
yup: yup:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
zod:
specifier: ^3.22.2
version: 3.22.2
zustand: zustand:
specifier: ^4.4.1 specifier: ^4.4.1
version: 4.4.1(@types/react@18.2.21)(react@18.2.0) 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) use-sync-external-store: 1.2.0(react@18.2.0)
dev: false 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: /@tsconfig/node10@1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@ -6145,6 +6185,10 @@ packages:
type-fest: 2.19.0 type-fest: 2.19.0
dev: false 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): /zustand@4.4.1(@types/react@18.2.21)(react@18.2.0):
resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==} resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
engines: {node: '>=12.7.0'} engines: {node: '>=12.7.0'}

View File

@ -12,6 +12,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './api/cache'; import { queryClient } from './api/cache';
import { TokenLoginContainer } from './components/TokenLoginContainer'; import { TokenLoginContainer } from './components/TokenLoginContainer';
import React from 'react'; import React from 'react';
import { trpc, trpcClient } from './api/trpc';
export const AppRoutes: React.FC = React.memo(() => { export const AppRoutes: React.FC = React.memo(() => {
const { info } = useUserStore(); const { info } = useUserStore();
@ -47,6 +48,7 @@ AppRoutes.displayName = 'AppRoutes';
export const App: React.FC = React.memo(() => { export const App: React.FC = React.memo(() => {
return ( return (
<div className="App"> <div className="App">
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
<TokenLoginContainer> <TokenLoginContainer>
@ -54,6 +56,7 @@ export const App: React.FC = React.memo(() => {
</TokenLoginContainer> </TokenLoginContainer>
</BrowserRouter> </BrowserRouter>
</QueryClientProvider> </QueryClientProvider>
</trpc.Provider>
</div> </div>
); );
}); });

19
src/client/api/trpc.ts Normal file
View 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()}`,
};
},
}),
],
});

View File

@ -11,6 +11,7 @@ import { websiteRouter } from './router/website';
import { workspaceRouter } from './router/workspace'; import { workspaceRouter } from './router/workspace';
import { telemetryRouter } from './router/telemetry'; import { telemetryRouter } from './router/telemetry';
import { initSocketio } from './ws'; import { initSocketio } from './ws';
import { trpcExpressMiddleware } from './trpc';
const port = Number(process.env.PORT || 12345); const port = Number(process.env.PORT || 12345);
@ -31,6 +32,8 @@ app.use('/api/website', websiteRouter);
app.use('/api/workspace', workspaceRouter); app.use('/api/workspace', workspaceRouter);
app.use('/api/telemetry', telemetryRouter); app.use('/api/telemetry', telemetryRouter);
app.use('/trpc', trpcExpressMiddleware);
app.use((err: any, req: any, res: any, next: any) => { app.use((err: any, req: any, res: any, next: any) => {
console.error(err); console.error(err);
res.status(500).json({ message: err.message }); res.status(500).json({ message: err.message });

16
src/server/trpc/index.ts Normal file
View 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
View 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();
});
}