mirror of
https://github.com/documenso/documenso.git
synced 2025-11-10 04:22:32 +10:00
Add ability to enable or disable allowed signature types: - Drawn - Typed - Uploaded **Tabbed style signature dialog**  **Document settings**  **Team preferences**  ## Changes Made - 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 ## Testing Performed 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);
|