import { TRPCError, initTRPC } from '@trpc/server'; import SuperJSON from 'superjson'; import type { AnyZodObject } from 'zod'; import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error'; import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { isAdmin } from '@documenso/lib/utils/is-admin'; import type { TrpcContext } from './context'; // Can't import type from trpc-to-openapi because it breaks build, not sure why. export type TrpcRouteMeta = { openapi?: { enabled?: boolean; method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; path: `/${string}`; summary?: string; description?: string; protect?: boolean; tags?: string[]; // eslint-disable-next-line @typescript-eslint/ban-types contentTypes?: ('application/json' | 'application/x-www-form-urlencoded' | (string & {}))[]; deprecated?: boolean; requestHeaders?: AnyZodObject; responseHeaders?: AnyZodObject; successDescription?: string; errorResponses?: number[] | Record; }; } & Record; const t = initTRPC .meta() .context() .create({ transformer: SuperJSON, errorFormatter(opts) { const { shape, error } = opts; const originalError = error.cause; let data: Record = shape.data; // Default unknown errors to 400, since if you're throwing an AppError it is expected // that you already know what you're doing. if (originalError instanceof AppError) { data = { ...data, appError: AppError.toJSON(originalError), code: originalError.code, httpStatus: originalError.statusCode ?? genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ?? 400, }; } return { ...shape, data, }; }, }); /** * Middlewares */ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => { const authorizationHeader = ctx.req.headers.get('authorization'); // Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`. if (authorizationHeader) { // Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx" const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0); if (!token) { throw new Error('Token was not provided for authenticated middleware'); } const apiToken = await getApiTokenByToken({ token }); return await next({ ctx: { ...ctx, user: apiToken.user, teamId: apiToken.teamId || undefined, session: null, metadata: { ...ctx.metadata, auditUser: apiToken.team ? { id: null, email: null, name: apiToken.team.name, } : { id: apiToken.user.id, email: apiToken.user.email, name: apiToken.user.name, }, auth: 'api', } satisfies ApiRequestMetadata, }, }); } if (!ctx.session) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Invalid session or API token.', }); } return await next({ ctx: { ...ctx, user: ctx.user, session: ctx.session, metadata: { ...ctx.metadata, auditUser: { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email, }, auth: 'session', } satisfies ApiRequestMetadata, }, }); }); export const maybeAuthenticatedMiddleware = t.middleware(async ({ ctx, next }) => { return await next({ ctx: { ...ctx, user: ctx.user, session: ctx.session, metadata: { ...ctx.metadata, auditUser: ctx.user ? { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email, } : undefined, auth: ctx.session ? 'session' : null, } satisfies ApiRequestMetadata, }, }); }); export const adminMiddleware = t.middleware(async ({ ctx, next }) => { if (!ctx.session || !ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You must be logged in to perform this action.', }); } const isUserAdmin = isAdmin(ctx.user); if (!isUserAdmin) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authorized to perform this action.', }); } return await next({ ctx: { ...ctx, user: ctx.user, session: ctx.session, metadata: { ...ctx.metadata, auditUser: { id: ctx.user.id, name: ctx.user.name, email: ctx.user.email, }, auth: 'session', } satisfies ApiRequestMetadata, }, }); }); /** * Routers and Procedures */ export const router = t.router; export const procedure = t.procedure; export const authenticatedProcedure = t.procedure.use(authenticatedMiddleware); // While this is functionally the same as `procedure`, it's useful for indicating purpose export const maybeAuthenticatedProcedure = t.procedure.use(maybeAuthenticatedMiddleware); export const adminProcedure = t.procedure.use(adminMiddleware);