tianji/src/server/trpc/trpc.ts

118 lines
3.0 KiB
TypeScript
Raw Normal View History

2023-09-27 09:56:32 +00:00
import { initTRPC, inferAsyncReturnType, TRPCError } from '@trpc/server';
2023-09-27 12:27:46 +00:00
import _ from 'lodash';
import { z } from 'zod';
2023-09-27 09:56:32 +00:00
import { jwtVerify } from '../middleware/auth';
import { getWorkspaceUser } from '../model/workspace';
import { ROLES, SYSTEM_ROLES } from '../utils/const';
2023-10-03 07:56:09 +00:00
import type { IncomingMessage } from 'http';
2023-10-21 16:26:13 +00:00
import { OpenApiMeta } from 'trpc-openapi';
2023-09-27 09:56:32 +00:00
2023-10-03 07:56:09 +00:00
export function createContext({ req }: { req: IncomingMessage }) {
2023-09-27 09:56:32 +00:00
const authorization = req.headers['authorization'] ?? '';
const token = authorization.replace('Bearer ', '');
2023-10-03 07:56:09 +00:00
return { token };
2023-09-27 09:56:32 +00:00
}
type Context = inferAsyncReturnType<typeof createContext>;
2023-10-21 16:26:13 +00:00
const t = initTRPC.context<Context>().meta<OpenApiMeta>().create();
2023-09-27 09:56:32 +00:00
export type OpenApiMetaInfo = NonNullable<OpenApiMeta['openapi']>;
2023-09-27 09:56:32 +00:00
export const middleware = t.middleware;
export const router = t.router;
export const publicProcedure = t.procedure;
2023-10-03 07:56:09 +00:00
const isUser = middleware(async (opts) => {
const token = opts.ctx.token;
if (!token) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'NoToken' });
}
try {
const user = jwtVerify(token);
return opts.next({
ctx: {
user,
},
});
} catch (err) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'TokenInvalid' });
}
});
export const protectProedure = t.procedure.use(isUser);
const isSystemAdmin = isUser.unstable_pipe(async (opts) => {
2023-09-27 09:56:32 +00:00
const { ctx, input } = opts;
2023-10-03 07:56:09 +00:00
2023-09-27 09:56:32 +00:00
if (ctx.user.role !== SYSTEM_ROLES.admin) {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return opts.next();
});
export const systemAdminProcedure = t.procedure.use(isSystemAdmin);
2023-10-03 07:56:09 +00:00
export const workspaceProcedure = protectProedure
2023-09-27 12:27:46 +00:00
.input(
z.object({
workspaceId: z.string().cuid2(),
2023-09-27 12:27:46 +00:00
})
)
.use(createWorkspacePermissionMiddleware());
2023-10-03 07:56:09 +00:00
export const workspaceOwnerProcedure = protectProedure
2023-09-27 12:27:46 +00:00
.input(
z.object({
workspaceId: z.string().cuid2(),
2023-09-27 12:27:46 +00:00
})
)
.use(createWorkspacePermissionMiddleware([ROLES.owner]));
2023-09-27 09:56:32 +00:00
/**
* Create a trpc middleware which help user check workspace permission
*/
function createWorkspacePermissionMiddleware(roles: ROLES[] = []) {
2023-10-03 07:56:09 +00:00
return isUser.unstable_pipe(async (opts) => {
2023-09-27 09:56:32 +00:00
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();
});
}