feat: add trpc framework
This commit is contained in:
parent
53c0fd563d
commit
b9b5ee5ae1
@ -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": {
|
||||||
|
@ -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'}
|
||||||
|
@ -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
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 { 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
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