mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
Add ability to enable or disable allowed signature types: - Drawn - Typed - Uploaded **Tabbed style signature dialog**  **Document settings**  **Team preferences**  - Add multiselect to select allowed signatures in document and templates settings tab - Add multiselect to select allowed signatures in teams preferences - Removed "Enable typed signatures" from document/template edit page - Refactored signature pad to use tabs instead of an all in one signature pad Added E2E tests to check settings are applied correctly for documents and templates
197 lines
5.4 KiB
TypeScript
197 lines
5.4 KiB
TypeScript
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<number, string>;
|
|
};
|
|
} & Record<string, unknown>;
|
|
|
|
const t = initTRPC
|
|
.meta<TrpcRouteMeta>()
|
|
.context<TrpcContext>()
|
|
.create({
|
|
transformer: SuperJSON,
|
|
errorFormatter(opts) {
|
|
const { shape, error } = opts;
|
|
|
|
const originalError = error.cause;
|
|
|
|
let data: Record<string, unknown> = 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);
|