fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-02-06 11:47:44 +00:00
783 changed files with 20849 additions and 22470 deletions

View File

@ -1,7 +0,0 @@
import { TRPCClientError } from '@trpc/client';
import { AppRouter } from '../server/router';
export const isTRPCBadRequestError = (err: unknown): err is TRPCClientError<AppRouter> => {
return err instanceof TRPCClientError && err.shape?.code === 'BAD_REQUEST';
};

View File

@ -1,24 +1,44 @@
import { createTRPCProxyClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
import { createTRPCClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router';
export const trpc = createTRPCProxyClient<AppRouter>({
transformer: SuperJSON,
export const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.context.skipBatch === true,
true: httpLink({
url: `${getBaseUrl()}/api/trpc`,
transformer: SuperJSON,
headers: (opts) => {
if (typeof opts.op.context.teamId === 'string') {
return {
'x-team-id': opts.op.context.teamId,
};
}
return {};
},
}),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
transformer: SuperJSON,
headers: (opts) => {
const operationWithTeamId = opts.opList.find(
(op) => op.context.teamId && typeof op.context.teamId === 'string',
);
if (operationWithTeamId && typeof operationWithTeamId.context.teamId === 'string') {
return {
'x-team-id': operationWithTeamId.context.teamId,
};
}
return {};
},
}),
}),
],
});
export { TRPCClientError } from '@trpc/client';

View File

@ -12,17 +12,17 @@
"dependencies": {
"@documenso/lib": "*",
"@documenso/prisma": "*",
"@tanstack/react-query": "^4.32.0",
"@trpc/client": "^10.36.0",
"@trpc/next": "^10.36.0",
"@trpc/react-query": "^10.36.0",
"@trpc/server": "^10.36.0",
"@tanstack/react-query": "5.59.15",
"@trpc/client": "11.0.0-rc.648",
"@trpc/next": "11.0.0-rc.648",
"@trpc/react-query": "11.0.0-rc.648",
"@trpc/server": "11.0.0-rc.648",
"@ts-rest/core": "^3.30.5",
"@ts-rest/next": "^3.30.5",
"luxon": "^3.4.0",
"superjson": "^1.13.1",
"trpc-to-openapi": "2.0.4",
"ts-pattern": "^5.0.5",
"zod": "^3.22.4"
},
"devDependencies": {}
"zod": "3.24.1"
}
}

View File

@ -35,9 +35,10 @@ export const trpc = createTRPCReact<AppRouter>({
export interface TrpcProviderProps {
children: React.ReactNode;
headers?: Record<string, string>;
}
export function TrpcProvider({ children }: TrpcProviderProps) {
export function TrpcProvider({ children, headers }: TrpcProviderProps) {
let queryClientConfig: QueryClientConfig | undefined;
const isDevelopingOffline =
@ -62,16 +63,18 @@ export function TrpcProvider({ children }: TrpcProviderProps) {
const [trpcClient] = useState(() =>
trpc.createClient({
transformer: SuperJSON,
links: [
splitLink({
condition: (op) => op.context.skipBatch === true,
true: httpLink({
url: `${getBaseUrl()}/api/trpc`,
headers,
transformer: SuperJSON,
}),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
headers,
transformer: SuperJSON,
}),
}),
],

View File

@ -1,5 +1,4 @@
import { TRPCError } from '@trpc/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
@ -9,6 +8,8 @@ import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete
import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { disableUser } from '@documenso/lib/server-only/user/disable-user';
import { enableUser } from '@documenso/lib/server-only/user/enable-user';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { DocumentStatus } from '@documenso/prisma/client';
@ -17,6 +18,8 @@ import { adminProcedure, router } from '../trpc';
import {
ZAdminDeleteDocumentMutationSchema,
ZAdminDeleteUserMutationSchema,
ZAdminDisableUserMutationSchema,
ZAdminEnableUserMutationSchema,
ZAdminFindDocumentsQuerySchema,
ZAdminResealDocumentMutationSchema,
ZAdminUpdateProfileMutationSchema,
@ -26,18 +29,9 @@ import {
export const adminRouter = router({
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
const { term, page, perPage } = input;
const { query, page, perPage } = input;
try {
return await findDocuments({ term, page, perPage });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to retrieve the documents. Please try again.',
});
}
return await findDocuments({ query, page, perPage });
}),
updateUser: adminProcedure
@ -45,16 +39,7 @@ export const adminRouter = router({
.mutation(async ({ input }) => {
const { id, name, email, roles } = input;
try {
return await updateUser({ id, name, email, roles });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to retrieve the specified account. Please try again.',
});
}
return await updateUser({ id, name, email, roles });
}),
updateRecipient: adminProcedure
@ -62,38 +47,20 @@ export const adminRouter = router({
.mutation(async ({ input }) => {
const { id, name, email } = input;
try {
return await updateRecipient({ id, name, email });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to update the recipient provided.',
});
}
return await updateRecipient({ id, name, email });
}),
updateSiteSetting: adminProcedure
.input(ZAdminUpdateSiteSettingMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { id, enabled, data } = input;
const { id, enabled, data } = input;
return await upsertSiteSetting({
id,
enabled,
data,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to update the site setting provided.',
});
}
return await upsertSiteSetting({
id,
enabled,
data,
userId: ctx.user.id,
});
}),
resealDocument: adminProcedure
@ -101,61 +68,56 @@ export const adminRouter = router({
.mutation(async ({ input }) => {
const { id } = input;
try {
const document = await getEntireDocument({ id });
const document = await getEntireDocument({ id });
const isResealing = document.status === DocumentStatus.COMPLETED;
const isResealing = document.status === DocumentStatus.COMPLETED;
return await sealDocument({ documentId: id, isResealing });
} catch (err) {
console.error('resealDocument error', err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to reseal the document provided.',
});
}
return await sealDocument({ documentId: id, isResealing });
}),
deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
const { id, email } = input;
enableUser: adminProcedure.input(ZAdminEnableUserMutationSchema).mutation(async ({ input }) => {
const { id } = input;
try {
const user = await getUserById({ id });
const user = await getUserById({ id }).catch(() => null);
if (user.email !== email) {
throw new Error('Email does not match');
}
return await deleteUser({ id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete the specified account. Please try again.',
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
return await enableUser({ id });
}),
disableUser: adminProcedure.input(ZAdminDisableUserMutationSchema).mutation(async ({ input }) => {
const { id } = input;
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
return await disableUser({ id });
}),
deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
const { id } = input;
return await deleteUser({ id });
}),
deleteDocument: adminProcedure
.input(ZAdminDeleteDocumentMutationSchema)
.mutation(async ({ ctx, input }) => {
const { id, reason } = input;
try {
await sendDeleteEmail({ documentId: id, reason });
await sendDeleteEmail({ documentId: id, reason });
return await superDeleteDocument({
id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete the specified document. Please try again.',
});
}
return await superDeleteDocument({
id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@ -2,10 +2,9 @@ import { Role } from '@prisma/client';
import z from 'zod';
import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const ZAdminFindDocumentsQuerySchema = z.object({
term: z.string().optional(),
page: z.number().optional().default(1),
export const ZAdminFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({
perPage: z.number().optional().default(20),
});
@ -44,11 +43,22 @@ export type TAdminResealDocumentMutationSchema = z.infer<typeof ZAdminResealDocu
export const ZAdminDeleteUserMutationSchema = z.object({
id: z.number().min(1),
email: z.string().email(),
});
export type TAdminDeleteUserMutationSchema = z.infer<typeof ZAdminDeleteUserMutationSchema>;
export const ZAdminEnableUserMutationSchema = z.object({
id: z.number().min(1),
});
export type TAdminEnableUserMutationSchema = z.infer<typeof ZAdminEnableUserMutationSchema>;
export const ZAdminDisableUserMutationSchema = z.object({
id: z.number().min(1),
});
export type TAdminDisableUserMutationSchema = z.infer<typeof ZAdminDisableUserMutationSchema>;
export const ZAdminDeleteDocumentMutationSchema = z.object({
id: z.number().min(1),
reason: z.string(),

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
import { getUserTokens } from '@documenso/lib/server-only/public-api/get-all-user-tokens';
@ -14,78 +12,42 @@ import {
export const apiTokenRouter = router({
getTokens: authenticatedProcedure.query(async ({ ctx }) => {
try {
return await getUserTokens({ userId: ctx.user.id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find your API tokens. Please try again.',
});
}
return await getUserTokens({ userId: ctx.user.id });
}),
getTokenById: authenticatedProcedure
.input(ZGetApiTokenByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { id } = input;
const { id } = input;
return await getApiTokenById({
id,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this API token. Please try again.',
});
}
return await getApiTokenById({
id,
userId: ctx.user.id,
});
}),
createToken: authenticatedProcedure
.input(ZCreateTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { tokenName, teamId, expirationDate } = input;
const { tokenName, teamId, expirationDate } = input;
return await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
expiresIn: expirationDate,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create an API token. Please try again.',
});
}
return await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
expiresIn: expirationDate,
});
}),
deleteTokenById: authenticatedProcedure
.input(ZDeleteTokenByIdMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id, teamId } = input;
const { id, teamId } = input;
return await deleteTokenById({
id,
teamId,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this API Token. Please try again.',
});
}
return await deleteTokenById({
id,
teamId,
userId: ctx.user.id,
});
}),
});

View File

@ -4,6 +4,7 @@ import { parse } from 'cookie-es';
import { env } from 'next-runtime-env';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { formatSecureCookieName } from '@documenso/lib/constants/auth';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
import { ErrorCode } from '@documenso/lib/next-auth/error-codes';
@ -16,7 +17,6 @@ import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
import { compareSync } from '@documenso/lib/server-only/auth/hash';
import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
import { createUser } from '@documenso/lib/server-only/user/create-user';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
@ -33,54 +33,30 @@ const NEXT_PUBLIC_DISABLE_SIGNUP = () => env('NEXT_PUBLIC_DISABLE_SIGNUP');
export const authRouter = router({
signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => {
try {
if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Signups are disabled.',
});
}
const { name, email, password, signature, url } = input;
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError(
AppErrorCode.PREMIUM_PROFILE_URL,
'Only subscribers can have a username shorter than 6 characters',
);
}
const user = await createUser({ name, email, password, signature, url });
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email: user.email,
},
});
return user;
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw AppError.parseErrorToTRPCError(error);
}
let message =
'We were unable to create your account. Please review the information you provided and try again.';
if (err instanceof Error && err.message === 'User already exists') {
message = 'User with this email already exists. Please use a different email address.';
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
if (NEXT_PUBLIC_DISABLE_SIGNUP() === 'true') {
throw new AppError('SIGNUP_DISABLED', {
message: 'Signups are disabled.',
});
}
const { name, email, password, signature, url } = input;
if (IS_BILLING_ENABLED() && url && url.length < 6) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
const user = await createUser({ name, email, password, signature, url });
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email: user.email,
},
});
return user;
}),
verifyPassword: authenticatedProcedure
@ -105,63 +81,38 @@ export const authRouter = router({
createPasskey: authenticatedProcedure
.input(ZCreatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const verificationResponse = input.verificationResponse as RegistrationResponseJSON;
return await createPasskey({
userId: ctx.user.id,
verificationResponse,
passkeyName: input.passkeyName,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
return await createPasskey({
userId: ctx.user.id,
verificationResponse,
passkeyName: input.passkeyName,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
createPasskeyAuthenticationOptions: authenticatedProcedure
.input(ZCreatePasskeyAuthenticationOptionsMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
preferredPasskeyId: input?.preferredPasskeyId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to create the authentication options for the passkey. Please try again later.',
});
}
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
preferredPasskeyId: input?.preferredPasskeyId,
});
}),
createPasskeyRegistrationOptions: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
return await createPasskeyRegistrationOptions({
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to create the registration options for the passkey. Please try again later.',
});
}
return await createPasskeyRegistrationOptions({
userId: ctx.user.id,
});
}),
createPasskeySigninOptions: procedure.mutation(async ({ ctx }) => {
const cookies = parse(ctx.req.headers.cookie ?? '');
const sessionIdToken =
cookies['__Host-next-auth.csrf-token'] || cookies['next-auth.csrf-token'];
cookies[formatSecureCookieName('__Host-next-auth.csrf-token')] ||
cookies[formatSecureCookieName('next-auth.csrf-token')];
if (!sessionIdToken) {
throw new Error('Missing CSRF token');
@ -169,80 +120,44 @@ export const authRouter = router({
const [sessionId] = decodeURI(sessionIdToken).split('|');
try {
return await createPasskeySigninOptions({ sessionId });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create the options for passkey signin. Please try again later.',
});
}
return await createPasskeySigninOptions({ sessionId });
}),
deletePasskey: authenticatedProcedure
.input(ZDeletePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { passkeyId } = input;
const { passkeyId } = input;
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this passkey. Please try again later.',
});
}
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
findPasskeys: authenticatedProcedure
.input(ZFindPasskeysQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { page, perPage, orderBy } = input;
const { page, perPage, orderBy } = input;
return await findPasskeys({
page,
perPage,
orderBy,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find passkeys. Please try again later.',
});
}
return await findPasskeys({
page,
perPage,
orderBy,
userId: ctx.user.id,
});
}),
updatePasskey: authenticatedProcedure
.input(ZUpdatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { passkeyId, name } = input;
const { passkeyId, name } = input;
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to update this passkey. Please try again later.',
});
}
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
});

View File

@ -1,6 +1,6 @@
import { z } from 'zod';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn';
export const ZCurrentPasswordSchema = z
@ -10,14 +10,20 @@ export const ZCurrentPasswordSchema = z
export const ZPasswordSchema = z
.string()
.regex(new RegExp('.*[A-Z].*'), { message: 'One uppercase character' })
.regex(new RegExp('.*[a-z].*'), { message: 'One lowercase character' })
.regex(new RegExp('.*\\d.*'), { message: 'One number' })
.regex(new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'), {
message: 'One special character is required',
})
.min(8, { message: 'Must be at least 8 characters in length' })
.max(72, { message: 'Cannot be more than 72 characters in length' });
.max(72, { message: 'Cannot be more than 72 characters in length' })
.refine((value) => value.length > 25 || /[A-Z]/.test(value), {
message: 'One uppercase character',
})
.refine((value) => value.length > 25 || /[a-z]/.test(value), {
message: 'One lowercase character',
})
.refine((value) => value.length > 25 || /\d/.test(value), {
message: 'One number',
})
.refine((value) => value.length > 25 || /[`~<>?,./!@#$%^&*()\-_"'+=|{}[\];:\\]/.test(value), {
message: 'One special character is required',
});
export const ZSignUpMutationSchema = z.object({
name: z.string().min(1),
@ -55,7 +61,7 @@ export const ZUpdatePasskeyMutationSchema = z.object({
name: z.string().trim().min(1),
});
export const ZFindPasskeysQuerySchema = ZBaseTableSearchParamsSchema.extend({
export const ZFindPasskeysQuerySchema = ZFindSearchParamsSchema.extend({
orderBy: z
.object({
column: z.enum(['createdAt', 'updatedAt', 'name']),

View File

@ -1,15 +1,41 @@
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { z } from 'zod';
import { getServerSession } from '@documenso/lib/next-auth/get-server-session';
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
export const createTrpcContext = async ({ req, res }: CreateNextContextOptions) => {
import type { CreateNextContextOptions } from './adapters/next';
type CreateTrpcContext = CreateNextContextOptions & {
requestSource: 'apiV1' | 'apiV2' | 'app';
};
export const createTrpcContext = async ({
req,
res,
requestSource,
}: Omit<CreateTrpcContext, 'info'>) => {
const { session, user } = await getServerSession({ req, res });
const metadata: ApiRequestMetadata = {
requestMetadata: extractNextApiRequestMetadata(req),
source: requestSource,
auth: null,
};
const teamId = z.coerce
.number()
.optional()
.catch(() => undefined)
.parse(req.headers['x-team-id']);
if (!session) {
return {
session: null,
user: null,
teamId,
req,
metadata,
};
}
@ -17,14 +43,18 @@ export const createTrpcContext = async ({ req, res }: CreateNextContextOptions)
return {
session: null,
user: null,
teamId,
req,
metadata,
};
}
return {
session,
user,
teamId,
req,
metadata,
};
};

View File

@ -1,8 +0,0 @@
import { procedure, router } from '../trpc';
import { ZEncryptSecondaryDataMutationSchema } from './schema';
export const cryptoRouter = router({
encryptSecondaryData: procedure.input(ZEncryptSecondaryDataMutationSchema).mutation(() => {
throw new Error('Public usage of encryptSecondaryData is no longer permitted');
}),
});

View File

@ -1,15 +0,0 @@
import { z } from 'zod';
export const ZEncryptSecondaryDataMutationSchema = z.object({
data: z.string(),
expiresAt: z.number().optional(),
});
export const ZDecryptDataMutationSchema = z.object({
data: z.string(),
});
export type TEncryptSecondaryDataMutationSchema = z.infer<
typeof ZEncryptSecondaryDataMutationSchema
>;
export type TDecryptDataMutationSchema = z.infer<typeof ZDecryptDataMutationSchema>;

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,27 @@
import { z } from 'zod';
import { VALID_DATE_FORMAT_VALUES } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import {
ZDocumentLiteSchema,
ZDocumentManySchema,
ZDocumentSchema,
} from '@documenso/lib/types/document';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import {
DocumentDistributionMethod,
@ -15,44 +30,126 @@ import {
DocumentStatus,
DocumentVisibility,
FieldType,
RecipientRole,
} from '@documenso/prisma/client';
export const ZFindDocumentsQuerySchema = ZBaseTableSearchParamsSchema.extend({
teamId: z.number().min(1).optional(),
templateId: z.number().min(1).optional(),
search: z
.string()
.optional()
.catch(() => undefined),
source: z.nativeEnum(DocumentSource).optional(),
status: z.nativeEnum(DocumentStatus).optional(),
orderBy: z
.object({
column: z.enum(['createdAt']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
}).omit({ query: true });
import { ZCreateRecipientSchema } from '../recipient-router/schema';
export const ZFindDocumentAuditLogsQuerySchema = ZBaseTableSearchParamsSchema.extend({
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
*
* Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response.
*/
export const ZSuccessResponseSchema = z.object({
success: z.literal(true),
});
export const ZGenericSuccessResponse = {
success: true,
} satisfies z.infer<typeof ZSuccessResponseSchema>;
export const ZDocumentTitleSchema = z
.string()
.trim()
.min(1)
.max(255)
.describe('The title of the document.');
export const ZDocumentExternalIdSchema = z
.string()
.trim()
.describe('The external ID of the document.');
export const ZDocumentVisibilitySchema = z
.nativeEnum(DocumentVisibility)
.describe('The visibility of the document.');
export const ZDocumentMetaTimezoneSchema = z
.string()
.describe(
'The timezone to use for date fields and signing the document. Example Etc/UTC, Australia/Melbourne',
);
// Cooked.
// .refine((value) => TIME_ZONES.includes(value), {
// message: 'Invalid timezone. Please provide a valid timezone',
// });
export type TDocumentMetaTimezone = z.infer<typeof ZDocumentMetaTimezoneSchema>;
export const ZDocumentMetaDateFormatSchema = z
.enum(VALID_DATE_FORMAT_VALUES)
.describe('The date format to use for date fields and signing the document.');
export type TDocumentMetaDateFormat = z.infer<typeof ZDocumentMetaDateFormatSchema>;
export const ZDocumentMetaRedirectUrlSchema = z
.string()
.describe('The URL to which the recipient should be redirected after signing the document.')
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message: 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
});
export const ZDocumentMetaLanguageSchema = z
.enum(SUPPORTED_LANGUAGE_CODES)
.describe('The language to use for email communications with recipients.');
export const ZDocumentMetaSubjectSchema = z
.string()
.describe('The subject of the email that will be sent to the recipients.');
export const ZDocumentMetaMessageSchema = z
.string()
.describe('The message of the email that will be sent to the recipients.');
export const ZDocumentMetaDistributionMethodSchema = z
.nativeEnum(DocumentDistributionMethod)
.describe('The distribution method to use when sending the document to the recipients.');
export const ZDocumentMetaTypedSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using a typed signature.');
export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
templateId: z
.number()
.describe('Filter documents by the template ID used to create it.')
.optional(),
source: z
.nativeEnum(DocumentSource)
.describe('Filter documents by how it was created.')
.optional(),
status: z
.nativeEnum(DocumentStatus)
.describe('Filter documents by the current status')
.optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
});
export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
data: ZDocumentManySchema.array(),
});
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
documentId: z.number().min(1),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
orderBy: z
.object({
column: z.enum(['createdAt', 'type']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
});
export const ZGetDocumentByIdQuerySchema = z.object({
id: z.number().min(1),
teamId: z.number().min(1).optional(),
documentId: z.number(),
});
export type TGetDocumentByIdQuerySchema = z.infer<typeof ZGetDocumentByIdQuerySchema>;
export const ZDuplicateDocumentRequestSchema = z.object({
documentId: z.number(),
});
export const ZDuplicateDocumentResponseSchema = z.object({
documentId: z.number(),
});
export const ZGetDocumentByTokenQuerySchema = z.object({
token: z.string().min(1),
@ -60,75 +157,105 @@ export const ZGetDocumentByTokenQuerySchema = z.object({
export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQuerySchema>;
export const ZGetDocumentWithDetailsByIdQuerySchema = z.object({
id: z.number().min(1),
teamId: z.number().min(1).optional(),
export const ZGetDocumentWithDetailsByIdRequestSchema = z.object({
documentId: z.number(),
});
export type TGetDocumentWithDetailsByIdQuerySchema = z.infer<
typeof ZGetDocumentWithDetailsByIdQuerySchema
>;
export const ZGetDocumentWithDetailsByIdResponseSchema = ZDocumentSchema;
export const ZCreateDocumentMutationSchema = z.object({
title: z.string().min(1),
export const ZCreateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string().min(1),
teamId: z.number().optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
});
export type TCreateDocumentMutationSchema = z.infer<typeof ZCreateDocumentMutationSchema>;
export const ZSetSettingsForDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
data: z.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
}),
meta: z.object({
timezone: z.string(),
dateFormat: z.string(),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
export const ZCreateDocumentV2RequestSchema = z.object({
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
formValues: ZDocumentFormValuesSchema.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
}),
)
.refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{ message: 'Recipients must have unique emails' },
)
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export type TSetGeneralSettingsForDocumentMutationSchema = z.infer<
typeof ZSetSettingsForDocumentMutationSchema
>;
export type TCreateDocumentV2Request = z.infer<typeof ZCreateDocumentV2RequestSchema>;
export const ZSetTitleForDocumentMutationSchema = z.object({
export const ZCreateDocumentV2ResponseSchema = z.object({
document: ZDocumentSchema,
uploadUrl: z
.string()
.describe(
'The URL to upload the document PDF to. Use a PUT request with the file via form-data',
),
});
export const ZUpdateDocumentRequestSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
title: z.string().min(1),
data: z
.object({
title: ZDocumentTitleSchema.optional(),
externalId: ZDocumentExternalIdSchema.nullish(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export type TSetTitleForDocumentMutationSchema = z.infer<typeof ZSetTitleForDocumentMutationSchema>;
export const ZSetRecipientsForDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().min(1).optional(),
recipients: z.array(
z.object({
id: z.number().nullish(),
email: z.string().min(1).email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
}),
),
});
export type TSetRecipientsForDocumentMutationSchema = z.infer<
typeof ZSetRecipientsForDocumentMutationSchema
>;
export const ZUpdateDocumentResponseSchema = ZDocumentLiteSchema;
export const ZSetFieldsForDocumentMutationSchema = z.object({
documentId: z.number(),
@ -150,24 +277,20 @@ export type TSetFieldsForDocumentMutationSchema = z.infer<
typeof ZSetFieldsForDocumentMutationSchema
>;
export const ZSendDocumentMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
meta: z.object({
subject: z.string(),
message: z.string(),
timezone: z.string().optional(),
dateFormat: z.string().optional(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
}),
export const ZDistributeDocumentRequestSchema = z.object({
documentId: z.number().describe('The ID of the document to send.'),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZSelfSignDocumentMutationSchema = z.object({
@ -175,6 +298,8 @@ export const ZSelfSignDocumentMutationSchema = z.object({
teamId: z.number().optional(),
});
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
export const ZSetPasswordForDocumentMutationSchema = z.object({
documentId: z.number(),
password: z.string(),
@ -193,30 +318,19 @@ export type TSetSigningOrderForDocumentMutationSchema = z.infer<
typeof ZSetSigningOrderForDocumentMutationSchema
>;
export const ZUpdateTypedSignatureSettingsMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
typedSignatureEnabled: z.boolean(),
});
export type TUpdateTypedSignatureSettingsMutationSchema = z.infer<
typeof ZUpdateTypedSignatureSettingsMutationSchema
>;
export const ZResendDocumentMutationSchema = z.object({
documentId: z.number(),
recipients: z.array(z.number()).min(1),
teamId: z.number().min(1).optional(),
recipients: z
.array(z.number())
.min(1)
.describe('The IDs of the recipients to redistribute the document to.'),
});
export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSchema>;
export const ZDeleteDraftDocumentMutationSchema = z.object({
id: z.number().min(1),
teamId: z.number().min(1).optional(),
export const ZDeleteDocumentMutationSchema = z.object({
documentId: z.number(),
});
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
export type TDeleteDocumentMutationSchema = z.infer<typeof ZDeleteDocumentMutationSchema>;
export const ZSearchDocumentsMutationSchema = z.object({
query: z.string(),
@ -224,15 +338,15 @@ export const ZSearchDocumentsMutationSchema = z.object({
export const ZDownloadAuditLogsMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
});
export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(),
teamId: z.number().optional(),
});
export const ZMoveDocumentsToTeamSchema = z.object({
documentId: z.number(),
teamId: z.number(),
export const ZMoveDocumentToTeamSchema = z.object({
documentId: z.number().describe('The ID of the document to move to a team.'),
teamId: z.number().describe('The ID of the team to move the document to.'),
});
export const ZMoveDocumentToTeamResponseSchema = ZDocumentLiteSchema;

View File

@ -1,168 +1,477 @@
import { TRPCError } from '@trpc/server';
import { AppError } from '@documenso/lib/errors/app-error';
import { createDocumentFields } from '@documenso/lib/server-only/field/create-document-fields';
import { createTemplateFields } from '@documenso/lib/server-only/field/create-template-fields';
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
import { deleteTemplateField } from '@documenso/lib/server-only/field/delete-template-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token';
import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
import { updateDocumentFields } from '@documenso/lib/server-only/field/update-document-fields';
import { updateTemplateFields } from '@documenso/lib/server-only/field/update-template-fields';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZAddFieldsMutationSchema,
ZAddTemplateFieldsMutationSchema,
ZGetFieldQuerySchema,
ZCreateDocumentFieldRequestSchema,
ZCreateDocumentFieldResponseSchema,
ZCreateDocumentFieldsRequestSchema,
ZCreateDocumentFieldsResponseSchema,
ZCreateTemplateFieldRequestSchema,
ZCreateTemplateFieldResponseSchema,
ZCreateTemplateFieldsRequestSchema,
ZCreateTemplateFieldsResponseSchema,
ZDeleteDocumentFieldRequestSchema,
ZDeleteTemplateFieldRequestSchema,
ZGetFieldRequestSchema,
ZGetFieldResponseSchema,
ZRemovedSignedFieldWithTokenMutationSchema,
ZSetDocumentFieldsRequestSchema,
ZSetDocumentFieldsResponseSchema,
ZSetFieldsForTemplateRequestSchema,
ZSetFieldsForTemplateResponseSchema,
ZSignFieldWithTokenMutationSchema,
ZUpdateDocumentFieldRequestSchema,
ZUpdateDocumentFieldResponseSchema,
ZUpdateDocumentFieldsRequestSchema,
ZUpdateDocumentFieldsResponseSchema,
ZUpdateTemplateFieldRequestSchema,
ZUpdateTemplateFieldResponseSchema,
ZUpdateTemplateFieldsRequestSchema,
ZUpdateTemplateFieldsResponseSchema,
} from './schema';
export const fieldRouter = router({
addFields: authenticatedProcedure
.input(ZAddFieldsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, fields } = input;
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set this field. Please try again later.',
});
}
}),
addTemplateFields: authenticatedProcedure
.input(ZAddTemplateFieldsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, fields } = input;
try {
return await setFieldsForTemplate({
userId: ctx.user.id,
templateId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
});
} catch (err) {
console.error(err);
throw err;
}
}),
signFieldWithToken: procedure
.input(ZSignFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, fieldId, value, isBase64, authOptions } = input;
return await signFieldWithToken({
token,
fieldId,
value,
isBase64,
userId: ctx.user?.id,
authOptions,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
}),
removeSignedFieldWithToken: procedure
.input(ZRemovedSignedFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, fieldId } = input;
return await removeSignedFieldWithToken({
token,
fieldId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to remove the signature for this field. Please try again later.',
});
}
}),
getField: authenticatedProcedure.input(ZGetFieldQuerySchema).query(async ({ input, ctx }) => {
try {
const { fieldId, teamId } = input;
/**
* @public
*/
getDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/field/{fieldId}',
summary: 'Get document field',
description:
'Returns a single field. If you want to retrieve all the fields for a document, use the "Get Document" endpoint.',
tags: ['Document Fields'],
},
})
.input(ZGetFieldRequestSchema)
.output(ZGetFieldResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { fieldId } = input;
return await getFieldById({
userId: ctx.user.id,
teamId,
fieldId,
});
} catch (err) {
console.error(err);
}),
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this field. Please try again.',
/**
* @public
*/
createDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/create',
summary: 'Create document field',
description: 'Create a single field for a document.',
tags: ['Document Fields'],
},
})
.input(ZCreateDocumentFieldRequestSchema)
.output(ZCreateDocumentFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, field } = input;
const createdFields = await createDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields: [field],
requestMetadata: ctx.metadata,
});
}
}),
// This doesn't appear to be used anywhere, and it doesn't seem to support updating template fields
// so commenting this out for now.
// updateField: authenticatedProcedure
// .input(ZUpdateFieldMutationSchema)
// .mutation(async ({ input, ctx }) => {
// try {
// const { documentId, fieldId, fieldMeta, teamId } = input;
return createdFields.fields[0];
}),
// return await updateField({
// userId: ctx.user.id,
// teamId,
// fieldId,
// documentId,
// requestMetadata: extractNextApiRequestMetadata(ctx.req),
// fieldMeta: fieldMeta,
// });
// } catch (err) {
// console.error(err);
/**
* @public
*/
createDocumentFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/create-many',
summary: 'Create document fields',
description: 'Create multiple fields for a document.',
tags: ['Document Fields'],
},
})
.input(ZCreateDocumentFieldsRequestSchema)
.output(ZCreateDocumentFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, fields } = input;
// throw new TRPCError({
// code: 'BAD_REQUEST',
// message: 'We were unable to set this field. Please try again later.',
// });
// }
// }),
return await createDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
updateDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/update',
summary: 'Update document field',
description: 'Update a single field for a document.',
tags: ['Document Fields'],
},
})
.input(ZUpdateDocumentFieldRequestSchema)
.output(ZUpdateDocumentFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, field } = input;
const updatedFields = await updateDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields: [field],
requestMetadata: ctx.metadata,
});
return updatedFields.fields[0];
}),
/**
* @public
*/
updateDocumentFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/update-many',
summary: 'Update document fields',
description: 'Update multiple fields for a document.',
tags: ['Document Fields'],
},
})
.input(ZUpdateDocumentFieldsRequestSchema)
.output(ZUpdateDocumentFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, fields } = input;
return await updateDocumentFields({
userId: ctx.user.id,
teamId,
documentId,
fields,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
deleteDocumentField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/field/delete',
summary: 'Delete document field',
tags: ['Document Fields'],
},
})
.input(ZDeleteDocumentFieldRequestSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { fieldId } = input;
await deleteDocumentField({
userId: ctx.user.id,
teamId,
fieldId,
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
}),
/**
* @private
*
* Todo: Refactor to setFieldsForDocument function.
*/
addFields: authenticatedProcedure
.input(ZSetDocumentFieldsRequestSchema)
.output(ZSetDocumentFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, fields } = input;
return await setFieldsForDocument({
documentId,
userId: ctx.user.id,
teamId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
createTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/create',
summary: 'Create template field',
description: 'Create a single field for a template.',
tags: ['Template Fields'],
},
})
.input(ZCreateTemplateFieldRequestSchema)
.output(ZCreateTemplateFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, field } = input;
const createdFields = await createTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields: [field],
});
return createdFields.fields[0];
}),
/**
* @public
*/
getTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/template/field/{fieldId}',
summary: 'Get template field',
description:
'Returns a single field. If you want to retrieve all the fields for a template, use the "Get Template" endpoint.',
tags: ['Template Fields'],
},
})
.input(ZGetFieldRequestSchema)
.output(ZGetFieldResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { fieldId } = input;
return await getFieldById({
userId: ctx.user.id,
teamId,
fieldId,
});
}),
/**
* @public
*/
createTemplateFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/create-many',
summary: 'Create template fields',
description: 'Create multiple fields for a template.',
tags: ['Template Fields'],
},
})
.input(ZCreateTemplateFieldsRequestSchema)
.output(ZCreateTemplateFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, fields } = input;
return await createTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields,
});
}),
/**
* @public
*/
updateTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/update',
summary: 'Update template field',
description: 'Update a single field for a template.',
tags: ['Template Fields'],
},
})
.input(ZUpdateTemplateFieldRequestSchema)
.output(ZUpdateTemplateFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, field } = input;
const updatedFields = await updateTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields: [field],
});
return updatedFields.fields[0];
}),
/**
* @public
*/
updateTemplateFields: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/update-many',
summary: 'Update template fields',
description: 'Update multiple fields for a template.',
tags: ['Template Fields'],
},
})
.input(ZUpdateTemplateFieldsRequestSchema)
.output(ZUpdateTemplateFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, fields } = input;
return await updateTemplateFields({
userId: ctx.user.id,
teamId,
templateId,
fields,
});
}),
/**
* @public
*/
deleteTemplateField: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/field/delete',
summary: 'Delete template field',
tags: ['Template Fields'],
},
})
.input(ZDeleteTemplateFieldRequestSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { fieldId } = input;
await deleteTemplateField({
userId: ctx.user.id,
teamId,
fieldId,
});
return ZGenericSuccessResponse;
}),
/**
* @private
*
* Todo: Refactor to setFieldsForTemplate.
*/
addTemplateFields: authenticatedProcedure
.input(ZSetFieldsForTemplateRequestSchema)
.output(ZSetFieldsForTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, fields } = input;
return await setFieldsForTemplate({
templateId,
userId: ctx.user.id,
teamId,
fields: fields.map((field) => ({
id: field.nativeId,
signerEmail: field.signerEmail,
type: field.type,
pageNumber: field.pageNumber,
pageX: field.pageX,
pageY: field.pageY,
pageWidth: field.pageWidth,
pageHeight: field.pageHeight,
fieldMeta: field.fieldMeta,
})),
});
}),
/**
* @private
*/
signFieldWithToken: procedure
.input(ZSignFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
const { token, fieldId, value, isBase64, authOptions } = input;
return await signFieldWithToken({
token,
fieldId,
value,
isBase64,
userId: ctx.user?.id,
authOptions,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
/**
* @private
*/
removeSignedFieldWithToken: procedure
.input(ZRemovedSignedFieldWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
const { token, fieldId } = input;
return await removeSignedFieldWithToken({
token,
fieldId,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@ -1,10 +1,112 @@
import { z } from 'zod';
import { ZRecipientActionAuthSchema } from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { FieldType } from '@documenso/prisma/client';
export const ZAddFieldsMutationSchema = z.object({
const ZCreateFieldSchema = ZFieldAndMetaSchema.and(
z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for.'),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
);
const ZUpdateFieldSchema = ZFieldAndMetaSchema.and(
z.object({
id: z.number().describe('The ID of the field to update.'),
pageNumber: ZFieldPageNumberSchema.optional(),
pageX: ZFieldPageXSchema.optional(),
pageY: ZFieldPageYSchema.optional(),
width: ZFieldWidthSchema.optional(),
height: ZFieldHeightSchema.optional(),
}),
);
export const ZCreateDocumentFieldRequestSchema = z.object({
documentId: z.number(),
field: ZCreateFieldSchema,
});
export const ZCreateDocumentFieldResponseSchema = ZFieldSchema;
export const ZCreateDocumentFieldsRequestSchema = z.object({
documentId: z.number(),
fields: ZCreateFieldSchema.array(),
});
export const ZCreateDocumentFieldsResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export const ZUpdateDocumentFieldRequestSchema = z.object({
documentId: z.number(),
field: ZUpdateFieldSchema,
});
export const ZUpdateDocumentFieldResponseSchema = ZFieldSchema;
export const ZUpdateDocumentFieldsRequestSchema = z.object({
documentId: z.number(),
fields: ZUpdateFieldSchema.array(),
});
export const ZUpdateDocumentFieldsResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export const ZDeleteDocumentFieldRequestSchema = z.object({
fieldId: z.number(),
});
export const ZCreateTemplateFieldRequestSchema = z.object({
templateId: z.number(),
field: ZCreateFieldSchema,
});
export const ZCreateTemplateFieldResponseSchema = ZFieldSchema;
export const ZCreateTemplateFieldsRequestSchema = z.object({
templateId: z.number(),
fields: ZCreateFieldSchema.array(),
});
export const ZCreateTemplateFieldsResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export const ZUpdateTemplateFieldRequestSchema = z.object({
templateId: z.number(),
field: ZUpdateFieldSchema,
});
export const ZUpdateTemplateFieldsRequestSchema = z.object({
templateId: z.number(),
fields: ZUpdateFieldSchema.array(),
});
export const ZUpdateTemplateFieldsResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export const ZUpdateTemplateFieldResponseSchema = ZFieldSchema;
export const ZDeleteTemplateFieldRequestSchema = z.object({
fieldId: z.number(),
});
export const ZSetDocumentFieldsRequestSchema = z.object({
documentId: z.number(),
fields: z.array(
z.object({
@ -22,9 +124,11 @@ export const ZAddFieldsMutationSchema = z.object({
),
});
export type TAddFieldsMutationSchema = z.infer<typeof ZAddFieldsMutationSchema>;
export const ZSetDocumentFieldsResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export const ZAddTemplateFieldsMutationSchema = z.object({
export const ZSetFieldsForTemplateRequestSchema = z.object({
templateId: z.number(),
fields: z.array(
z.object({
@ -42,7 +146,9 @@ export const ZAddTemplateFieldsMutationSchema = z.object({
),
});
export type TAddTemplateFieldsMutationSchema = z.infer<typeof ZAddTemplateFieldsMutationSchema>;
export const ZSetFieldsForTemplateResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export const ZSignFieldWithTokenMutationSchema = z.object({
token: z.string(),
@ -63,16 +169,8 @@ export type TRemovedSignedFieldWithTokenMutationSchema = z.infer<
typeof ZRemovedSignedFieldWithTokenMutationSchema
>;
export const ZGetFieldQuerySchema = z.object({
export const ZGetFieldRequestSchema = z.object({
fieldId: z.number(),
teamId: z.number().optional(),
});
export type TGetFieldQuerySchema = z.infer<typeof ZGetFieldQuerySchema>;
export const ZUpdateFieldMutationSchema = z.object({
fieldId: z.number(),
documentId: z.number(),
fieldMeta: ZFieldMetaSchema,
teamId: z.number().optional(),
});
export const ZGetFieldResponseSchema = ZFieldSchema;

View File

@ -0,0 +1,30 @@
import { generateOpenApiDocument } from 'trpc-to-openapi';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { appRouter } from './router';
export const openApiDocument = {
...generateOpenApiDocument(appRouter, {
title: 'Documenso v2 beta API',
description: 'Subject to breaking changes until v2 is fully released.',
version: '0.0.0',
baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/v2-beta`,
securitySchemes: {
apiKey: {
type: 'apiKey',
in: 'header',
name: 'Authorization',
},
},
}),
/**
* Dirty way to pass through the security field.
*/
security: [
{
apiKey: [],
},
],
};

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobsClient } from '@documenso/lib/jobs/client';
@ -33,247 +31,122 @@ export const profileRouter = router({
findUserSecurityAuditLogs: authenticatedProcedure
.input(ZFindUserSecurityAuditLogsSchema)
.query(async ({ input, ctx }) => {
try {
return await findUserSecurityAuditLogs({
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find user security audit logs. Please try again.',
});
}
return await findUserSecurityAuditLogs({
userId: ctx.user.id,
...input,
});
}),
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input }) => {
try {
const { id } = input;
const { id } = input;
return await getUserById({ id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to retrieve the specified account. Please try again.',
});
}
return await getUserById({ id });
}),
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { name, signature } = input;
const { name, signature } = input;
return await updateProfile({
userId: ctx.user.id,
name,
signature,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update your profile. Please review the information you provided and try again.',
});
}
return await updateProfile({
userId: ctx.user.id,
name,
signature,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
updatePublicProfile: authenticatedProcedure
.input(ZUpdatePublicProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { url, bio, enabled } = input;
const { url, bio, enabled } = input;
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
const subscriptions = await getSubscriptionsByUserId({
userId: ctx.user.id,
}).then((subscriptions) =>
subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
);
if (subscriptions.length === 0) {
throw new AppError(
AppErrorCode.PREMIUM_PROFILE_URL,
'Only subscribers can have a username shorter than 6 characters',
);
}
}
const user = await updatePublicProfile({
if (IS_BILLING_ENABLED() && url !== undefined && url.length < 6) {
const subscriptions = await getSubscriptionsByUserId({
userId: ctx.user.id,
data: {
url,
bio,
enabled,
},
});
}).then((subscriptions) =>
subscriptions.filter((s) => s.status === SubscriptionStatus.ACTIVE),
);
return { success: true, url: user.url };
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
if (error.code !== AppErrorCode.UNKNOWN_ERROR) {
throw AppError.parseErrorToTRPCError(error);
if (subscriptions.length === 0) {
throw new AppError(AppErrorCode.PREMIUM_PROFILE_URL, {
message: 'Only subscribers can have a username shorter than 6 characters',
});
}
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update your public profile. Please review the information you provided and try again.',
});
}
const user = await updatePublicProfile({
userId: ctx.user.id,
data: {
url,
bio,
enabled,
},
});
return { success: true, url: user.url };
}),
updatePassword: authenticatedProcedure
.input(ZUpdatePasswordMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { password, currentPassword } = input;
const { password, currentPassword } = input;
return await updatePassword({
userId: ctx.user.id,
password,
currentPassword,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
let message =
'We were unable to update your profile. Please review the information you provided and try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
return await updatePassword({
userId: ctx.user.id,
password,
currentPassword,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
forgotPassword: procedure.input(ZForgotPasswordFormSchema).mutation(async ({ input }) => {
try {
const { email } = input;
const { email } = input;
return await forgotPassword({
email,
});
} catch (err) {
console.error(err);
}
return await forgotPassword({
email,
});
}),
resetPassword: procedure.input(ZResetPasswordFormSchema).mutation(async ({ input, ctx }) => {
try {
const { password, token } = input;
const { password, token } = input;
return await resetPassword({
token,
password,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
let message = 'We were unable to reset your password. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
return await resetPassword({
token,
password,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
sendConfirmationEmail: procedure
.input(ZConfirmEmailMutationSchema)
.mutation(async ({ input }) => {
try {
const { email } = input;
const { email } = input;
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email,
},
});
} catch (err) {
console.error(err);
let message = 'We were unable to send a confirmation email. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
await jobsClient.triggerJob({
name: 'send.signup.confirmation.email',
payload: {
email,
},
});
}),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
return await deleteUser({
id: ctx.user.id,
});
} catch (err) {
console.error(err);
let message = 'We were unable to delete your account. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
return await deleteUser({
id: ctx.user.id,
});
}),
setProfileImage: authenticatedProcedure
.input(ZSetProfileImageMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { bytes, teamId } = input;
const { bytes, teamId } = input;
return await setAvatarImage({
userId: ctx.user.id,
teamId,
bytes,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
let message = 'We were unable to update your profile image. Please try again.';
if (err instanceof Error) {
message = err.message;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
return await setAvatarImage({
userId: ctx.user.id,
teamId,
bytes,
requestMetadata: ctx.metadata,
});
}),
});

View File

@ -1,121 +1,466 @@
import { TRPCError } from '@trpc/server';
import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token';
import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template';
import { createDocumentRecipients } from '@documenso/lib/server-only/recipient/create-document-recipients';
import { createTemplateRecipients } from '@documenso/lib/server-only/recipient/create-template-recipients';
import { deleteDocumentRecipient } from '@documenso/lib/server-only/recipient/delete-document-recipient';
import { deleteTemplateRecipient } from '@documenso/lib/server-only/recipient/delete-template-recipient';
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients';
import { updateTemplateRecipients } from '@documenso/lib/server-only/recipient/update-template-recipients';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZAddSignersMutationSchema,
ZAddTemplateSignersMutationSchema,
ZCompleteDocumentWithTokenMutationSchema,
ZCreateDocumentRecipientRequestSchema,
ZCreateDocumentRecipientResponseSchema,
ZCreateDocumentRecipientsRequestSchema,
ZCreateDocumentRecipientsResponseSchema,
ZCreateTemplateRecipientRequestSchema,
ZCreateTemplateRecipientResponseSchema,
ZCreateTemplateRecipientsRequestSchema,
ZCreateTemplateRecipientsResponseSchema,
ZDeleteDocumentRecipientRequestSchema,
ZDeleteTemplateRecipientRequestSchema,
ZGetRecipientRequestSchema,
ZGetRecipientResponseSchema,
ZRejectDocumentWithTokenMutationSchema,
ZSetDocumentRecipientsRequestSchema,
ZSetDocumentRecipientsResponseSchema,
ZSetTemplateRecipientsRequestSchema,
ZSetTemplateRecipientsResponseSchema,
ZUpdateDocumentRecipientRequestSchema,
ZUpdateDocumentRecipientResponseSchema,
ZUpdateDocumentRecipientsRequestSchema,
ZUpdateDocumentRecipientsResponseSchema,
ZUpdateTemplateRecipientRequestSchema,
ZUpdateTemplateRecipientResponseSchema,
ZUpdateTemplateRecipientsRequestSchema,
ZUpdateTemplateRecipientsResponseSchema,
} from './schema';
export const recipientRouter = router({
addSigners: authenticatedProcedure
.input(ZAddSignersMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { documentId, teamId, signers } = input;
/**
* @public
*/
getDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/recipient/{recipientId}',
summary: 'Get document recipient',
description:
'Returns a single recipient. If you want to retrieve all the recipients for a document, use the "Get Document" endpoint.',
tags: ['Document Recipients'],
},
})
.input(ZGetRecipientRequestSchema)
.output(ZGetRecipientResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { recipientId } = input;
return await setRecipientsForDocument({
userId: ctx.user.id,
documentId,
teamId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set this field. Please try again later.',
});
}
return await getRecipientById({
userId: ctx.user.id,
teamId,
recipientId,
});
}),
addTemplateSigners: authenticatedProcedure
.input(ZAddTemplateSignersMutationSchema)
/**
* @public
*/
createDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/create',
summary: 'Create document recipient',
description: 'Create a single recipient for a document.',
tags: ['Document Recipients'],
},
})
.input(ZCreateDocumentRecipientRequestSchema)
.output(ZCreateDocumentRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, signers, teamId } = input;
const { teamId } = ctx;
const { documentId, recipient } = input;
return await setRecipientsForTemplate({
userId: ctx.user.id,
teamId,
templateId,
recipients: signers.map((signer) => ({
id: signer.nativeId,
email: signer.email,
name: signer.name,
role: signer.role,
signingOrder: signer.signingOrder,
actionAuth: signer.actionAuth,
})),
});
} catch (err) {
console.error(err);
const createdRecipients = await createDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients: [recipient],
requestMetadata: ctx.metadata,
});
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to set this field. Please try again later.',
});
}
return createdRecipients.recipients[0];
}),
/**
* @public
*/
createDocumentRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/create-many',
summary: 'Create document recipients',
description: 'Create multiple recipients for a document.',
tags: ['Document Recipients'],
},
})
.input(ZCreateDocumentRecipientsRequestSchema)
.output(ZCreateDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await createDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
updateDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/update',
summary: 'Update document recipient',
description: 'Update a single recipient for a document.',
tags: ['Document Recipients'],
},
})
.input(ZUpdateDocumentRecipientRequestSchema)
.output(ZUpdateDocumentRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipient } = input;
const updatedRecipients = await updateDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients: [recipient],
requestMetadata: ctx.metadata,
});
return updatedRecipients.recipients[0];
}),
/**
* @public
*/
updateDocumentRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/update-many',
summary: 'Update document recipients',
description: 'Update multiple recipients for a document.',
tags: ['Document Recipients'],
},
})
.input(ZUpdateDocumentRecipientsRequestSchema)
.output(ZUpdateDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await updateDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
deleteDocumentRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/recipient/delete',
summary: 'Delete document recipient',
tags: ['Document Recipients'],
},
})
.input(ZDeleteDocumentRecipientRequestSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { recipientId } = input;
await deleteDocumentRecipient({
userId: ctx.user.id,
teamId,
recipientId,
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
}),
/**
* @private
*/
setDocumentRecipients: authenticatedProcedure
.input(ZSetDocumentRecipientsRequestSchema)
.output(ZSetDocumentRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
return await setDocumentRecipients({
userId: ctx.user.id,
teamId,
documentId,
recipients: recipients.map((recipient) => ({
id: recipient.nativeId,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
actionAuth: recipient.actionAuth,
})),
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
getTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/template/recipient/{recipientId}',
summary: 'Get template recipient',
description:
'Returns a single recipient. If you want to retrieve all the recipients for a template, use the "Get Template" endpoint.',
tags: ['Template Recipients'],
},
})
.input(ZGetRecipientRequestSchema)
.output(ZGetRecipientResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { recipientId } = input;
return await getRecipientById({
userId: ctx.user.id,
teamId,
recipientId,
});
}),
/**
* @public
*/
createTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/create',
summary: 'Create template recipient',
description: 'Create a single recipient for a template.',
tags: ['Template Recipients'],
},
})
.input(ZCreateTemplateRecipientRequestSchema)
.output(ZCreateTemplateRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipient } = input;
const createdRecipients = await createTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients: [recipient],
});
return createdRecipients.recipients[0];
}),
/**
* @public
*/
createTemplateRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/create-many',
summary: 'Create template recipients',
description: 'Create multiple recipients for a template.',
tags: ['Template Recipients'],
},
})
.input(ZCreateTemplateRecipientsRequestSchema)
.output(ZCreateTemplateRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipients } = input;
return await createTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients,
});
}),
/**
* @public
*/
updateTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/update',
summary: 'Update template recipient',
description: 'Update a single recipient for a template.',
tags: ['Template Recipients'],
},
})
.input(ZUpdateTemplateRecipientRequestSchema)
.output(ZUpdateTemplateRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipient } = input;
const updatedRecipients = await updateTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients: [recipient],
});
return updatedRecipients.recipients[0];
}),
/**
* @public
*/
updateTemplateRecipients: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/update-many',
summary: 'Update template recipients',
description: 'Update multiple recipients for a template.',
tags: ['Template Recipients'],
},
})
.input(ZUpdateTemplateRecipientsRequestSchema)
.output(ZUpdateTemplateRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipients } = input;
return await updateTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients,
});
}),
/**
* @public
*/
deleteTemplateRecipient: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/recipient/delete',
summary: 'Delete template recipient',
tags: ['Template Recipients'],
},
})
.input(ZDeleteTemplateRecipientRequestSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { recipientId } = input;
await deleteTemplateRecipient({
recipientId,
userId: ctx.user.id,
teamId,
});
return ZGenericSuccessResponse;
}),
/**
* @private
*/
setTemplateRecipients: authenticatedProcedure
.input(ZSetTemplateRecipientsRequestSchema)
.output(ZSetTemplateRecipientsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, recipients } = input;
return await setTemplateRecipients({
userId: ctx.user.id,
teamId,
templateId,
recipients: recipients.map((recipient) => ({
id: recipient.nativeId,
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
actionAuth: recipient.actionAuth,
})),
});
}),
/**
* @private
*/
completeDocumentWithToken: procedure
.input(ZCompleteDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, documentId, authOptions } = input;
const { token, documentId, authOptions } = input;
return await completeDocumentWithToken({
token,
documentId,
authOptions,
userId: ctx.user?.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to sign this field. Please try again later.',
});
}
return await completeDocumentWithToken({
token,
documentId,
authOptions,
userId: ctx.user?.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
/**
* @private
*/
rejectDocumentWithToken: procedure
.input(ZRejectDocumentWithTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { token, documentId, reason } = input;
const { token, documentId, reason } = input;
return await rejectDocumentWithToken({
token,
documentId,
reason,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to handle this request. Please try again later.',
});
}
return await rejectDocumentWithToken({
token,
documentId,
reason,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
}),
});

View File

@ -1,19 +1,108 @@
import { z } from 'zod';
import {
ZRecipientAccessAuthTypesSchema,
ZRecipientActionAuthSchema,
ZRecipientActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZRecipientLiteSchema, ZRecipientSchema } from '@documenso/lib/types/recipient';
import { RecipientRole } from '@documenso/prisma/client';
export const ZAddSignersMutationSchema = z
export const ZGetRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZGetRecipientResponseSchema = ZRecipientSchema;
/**
* When changing this, ensure everything that uses this schema is updated correctly
* since this will change the Openapi schema.
*
* Example `createDocument` uses this, so you will need to update that function to
* pass along required details.
*/
export const ZCreateRecipientSchema = z.object({
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
});
const ZUpdateRecipientSchema = z.object({
id: z.number().describe('The ID of the recipient to update.'),
email: z.string().toLowerCase().email().min(1).optional(),
name: z.string().optional(),
role: z.nativeEnum(RecipientRole).optional(),
signingOrder: z.number().optional(),
accessAuth: ZRecipientAccessAuthTypesSchema.optional().nullable(),
actionAuth: ZRecipientActionAuthTypesSchema.optional().nullable(),
});
export const ZCreateDocumentRecipientRequestSchema = z.object({
documentId: z.number(),
recipient: ZCreateRecipientSchema,
});
export const ZCreateDocumentRecipientResponseSchema = ZRecipientLiteSchema;
export const ZCreateDocumentRecipientsRequestSchema = z.object({
documentId: z.number(),
recipients: z.array(ZCreateRecipientSchema).refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email.toLowerCase());
return new Set(emails).size === emails.length;
},
{
message: 'Recipients must have unique emails',
},
),
});
export const ZCreateDocumentRecipientsResponseSchema = z.object({
recipients: ZRecipientLiteSchema.array(),
});
export const ZUpdateDocumentRecipientRequestSchema = z.object({
documentId: z.number(),
recipient: ZUpdateRecipientSchema,
});
export const ZUpdateDocumentRecipientResponseSchema = ZRecipientSchema;
export const ZUpdateDocumentRecipientsRequestSchema = z.object({
documentId: z.number(),
recipients: z.array(ZUpdateRecipientSchema).refine(
(recipients) => {
const emails = recipients
.filter((recipient) => recipient.email !== undefined)
.map((recipient) => recipient.email?.toLowerCase());
return new Set(emails).size === emails.length;
},
{
message: 'Recipients must have unique emails',
},
),
});
export const ZUpdateDocumentRecipientsResponseSchema = z.object({
recipients: z.array(ZRecipientSchema),
});
export const ZDeleteDocumentRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZSetDocumentRecipientsRequestSchema = z
.object({
documentId: z.number(),
teamId: z.number().optional(),
signers: z.array(
recipients: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().email().min(1),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@ -23,24 +112,81 @@ export const ZAddSignersMutationSchema = z
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
const emails = schema.recipients.map((recipient) => recipient.email.toLowerCase());
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
);
export type TAddSignersMutationSchema = z.infer<typeof ZAddSignersMutationSchema>;
export const ZSetDocumentRecipientsResponseSchema = z.object({
recipients: ZRecipientLiteSchema.array(),
});
export const ZAddTemplateSignersMutationSchema = z
export const ZCreateTemplateRecipientRequestSchema = z.object({
templateId: z.number(),
recipient: ZCreateRecipientSchema,
});
export const ZCreateTemplateRecipientResponseSchema = ZRecipientLiteSchema;
export const ZCreateTemplateRecipientsRequestSchema = z.object({
templateId: z.number(),
recipients: z.array(ZCreateRecipientSchema).refine(
(recipients) => {
const emails = recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{
message: 'Recipients must have unique emails',
},
),
});
export const ZCreateTemplateRecipientsResponseSchema = z.object({
recipients: ZRecipientLiteSchema.array(),
});
export const ZUpdateTemplateRecipientRequestSchema = z.object({
templateId: z.number(),
recipient: ZUpdateRecipientSchema,
});
export const ZUpdateTemplateRecipientResponseSchema = ZRecipientSchema;
export const ZUpdateTemplateRecipientsRequestSchema = z.object({
templateId: z.number(),
recipients: z.array(ZUpdateRecipientSchema).refine(
(recipients) => {
const emails = recipients
.filter((recipient) => recipient.email !== undefined)
.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
{
message: 'Recipients must have unique emails',
},
),
});
export const ZUpdateTemplateRecipientsResponseSchema = z.object({
recipients: z.array(ZRecipientSchema),
});
export const ZDeleteTemplateRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZSetTemplateRecipientsRequestSchema = z
.object({
teamId: z.number().optional(),
templateId: z.number(),
signers: z.array(
recipients: z.array(
z.object({
nativeId: z.number().optional(),
email: z.string().email().min(1),
email: z.string().toLowerCase().email().min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@ -50,15 +196,17 @@ export const ZAddTemplateSignersMutationSchema = z
})
.refine(
(schema) => {
const emails = schema.signers.map((signer) => signer.email.toLowerCase());
const emails = schema.recipients.map((recipient) => recipient.email);
return new Set(emails).size === emails.length;
},
// Dirty hack to handle errors when .root is populated for an array type
{ message: 'Signers must have unique emails', path: ['signers__root'] },
{ message: 'Recipients must have unique emails', path: ['recipients__root'] },
);
export type TAddTemplateSignersMutationSchema = z.infer<typeof ZAddTemplateSignersMutationSchema>;
export const ZSetTemplateRecipientsResponseSchema = z.object({
recipients: ZRecipientLiteSchema.array(),
});
export const ZCompleteDocumentWithTokenMutationSchema = z.object({
token: z.string(),

View File

@ -1,13 +1,11 @@
import { adminRouter } from './admin-router/router';
import { apiTokenRouter } from './api-token-router/router';
import { authRouter } from './auth-router/router';
import { cryptoRouter } from './crypto/router';
import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router';
import { recipientRouter } from './recipient-router/router';
import { shareLinkRouter } from './share-link-router/router';
import { singleplayerRouter } from './singleplayer-router/router';
import { teamRouter } from './team-router/router';
import { templateRouter } from './template-router/router';
import { router } from './trpc';
@ -16,7 +14,6 @@ import { webhookRouter } from './webhook-router/router';
export const appRouter = router({
auth: authRouter,
crypto: cryptoRouter,
profile: profileRouter,
document: documentRouter,
field: fieldRouter,
@ -24,7 +21,6 @@ export const appRouter = router({
admin: adminRouter,
shareLink: shareLinkRouter,
apiToken: apiTokenRouter,
singleplayer: singleplayerRouter,
team: teamRouter,
template: templateRouter,
webhook: webhookRouter,

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link';
import { procedure, router } from '../trpc';
@ -9,27 +7,18 @@ export const shareLinkRouter = router({
createOrGetShareLink: procedure
.input(ZCreateOrGetShareLinkMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { documentId, token } = input;
const { documentId, token } = input;
if (token) {
return await createOrGetShareLink({ documentId, token });
}
if (!ctx.user?.id) {
throw new Error(
'You must either provide a token or be logged in to create a sharing link.',
);
}
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create a sharing link.',
});
if (token) {
return await createOrGetShareLink({ documentId, token });
}
if (!ctx.user?.id) {
throw new Error(
'You must either provide a token or be logged in to create a sharing link.',
);
}
return await createOrGetShareLink({ documentId, userId: ctx.user.id });
}),
});

View File

@ -1,42 +0,0 @@
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { FieldType, Prisma } from '@documenso/prisma/client';
import type { TCreateSinglePlayerDocumentMutationSchema } from './schema';
/**
* Map the fields provided by the user to fields compatible with Prisma.
*
* Signature fields are handled separately.
*
* @param field The field passed in by the user.
* @param signer The details of the person who is signing this document.
* @returns A field compatible with Prisma.
*/
export const mapField = (
field: TCreateSinglePlayerDocumentMutationSchema['fields'][number],
signer: TCreateSinglePlayerDocumentMutationSchema['signer'],
) => {
const customText = match(field.type)
.with(FieldType.DATE, () => DateTime.now().toFormat('yyyy-MM-dd hh:mm a'))
.with(FieldType.EMAIL, () => signer.email)
.with(FieldType.NAME, () => signer.name)
.with(FieldType.TEXT, () => signer.customText)
.with(FieldType.NUMBER, () => signer.customText)
.with(FieldType.RADIO, () => signer.customText)
.with(FieldType.CHECKBOX, () => signer.customText)
.with(FieldType.DROPDOWN, () => signer.customText)
.otherwise(() => '');
return {
type: field.type,
page: field.page,
positionX: new Prisma.Decimal(field.positionX),
positionY: new Prisma.Decimal(field.positionY),
width: new Prisma.Decimal(field.width),
height: new Prisma.Decimal(field.height),
customText,
inserted: true,
};
};

View File

@ -1,188 +0,0 @@
import { createElement } from 'react';
import { PDFDocument } from 'pdf-lib';
import { mailer } from '@documenso/email/mailer';
import { DocumentSelfSignedEmailTemplate } from '@documenso/email/templates/document-self-signed';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { FROM_ADDRESS, FROM_NAME, SERVICE_USER_EMAIL } from '@documenso/lib/constants/email';
import { insertFieldInPDF } from '@documenso/lib/server-only/pdf/insert-field-in-pdf';
import { alphaid } from '@documenso/lib/universal/id';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { renderEmailWithI18N } from '@documenso/lib/utils/render-email-with-i18n';
import { prisma } from '@documenso/prisma';
import {
DocumentSource,
DocumentStatus,
FieldType,
ReadStatus,
SendStatus,
SigningStatus,
} from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import { procedure, router } from '../trpc';
import { mapField } from './helper';
import { ZCreateSinglePlayerDocumentMutationSchema } from './schema';
export const singleplayerRouter = router({
createSinglePlayerDocument: procedure
.input(ZCreateSinglePlayerDocumentMutationSchema)
.mutation(async ({ input }) => {
try {
const { signer, fields, documentData, documentName, fieldMeta } = input;
const document = await getFile({
data: documentData.data,
type: documentData.type,
});
const doc = await PDFDocument.load(document);
const createdAt = new Date();
const isBase64 = signer.signature.startsWith('data:image/png;base64,');
const signatureImageAsBase64 = isBase64 ? signer.signature : null;
const typedSignature = !isBase64 ? signer.signature : null;
// Update the document with the fields inserted.
for (const field of fields) {
const isSignatureField = field.type === FieldType.SIGNATURE;
await insertFieldInPDF(doc, {
...mapField(field, signer),
Signature: isSignatureField
? {
created: createdAt,
signatureImageAsBase64,
typedSignature,
// Dummy data.
id: -1,
recipientId: -1,
fieldId: -1,
}
: null,
// Dummy data.
id: -1,
secondaryId: '-1',
documentId: -1,
templateId: null,
recipientId: -1,
fieldMeta: fieldMeta || null,
});
}
const unsignedPdfBytes = await doc.save();
const signedPdfBuffer = await signPdf({ pdf: Buffer.from(unsignedPdfBytes) });
const { token } = await prisma.$transaction(
async (tx) => {
const token = alphaid();
// Fetch service user who will be the owner of the document.
const serviceUser = await tx.user.findFirstOrThrow({
where: {
email: SERVICE_USER_EMAIL,
},
});
const { id: documentDataId } = await putPdfFile({
name: `${documentName}.pdf`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(signedPdfBuffer),
});
// Create document.
const document = await tx.document.create({
data: {
source: DocumentSource.DOCUMENT,
title: documentName,
status: DocumentStatus.COMPLETED,
documentDataId,
userId: serviceUser.id,
createdAt,
},
});
// Create recipient.
const recipient = await tx.recipient.create({
data: {
documentId: document.id,
name: signer.name,
email: signer.email,
token,
signedAt: createdAt,
readStatus: ReadStatus.OPENED,
signingStatus: SigningStatus.SIGNED,
sendStatus: SendStatus.SENT,
},
});
// Create fields and signatures.
await Promise.all(
fields.map(async (field) => {
const insertedField = await tx.field.create({
data: {
documentId: document.id,
recipientId: recipient.id,
...mapField(field, signer),
},
});
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await tx.signature.create({
data: {
fieldId: insertedField.id,
signatureImageAsBase64,
typedSignature,
recipientId: recipient.id,
},
});
}
}),
);
return { document, token };
},
{
maxWait: 5000,
timeout: 30000,
},
);
const template = createElement(DocumentSelfSignedEmailTemplate, {
documentName: documentName,
assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000',
});
const [html, text] = await Promise.all([
renderEmailWithI18N(template),
renderEmailWithI18N(template, { plainText: true }),
]);
// Send email to signer.
await mailer.sendMail({
to: {
address: signer.email,
name: signer.name,
},
from: {
name: FROM_NAME,
address: FROM_ADDRESS,
},
subject: 'Document signed',
html,
text,
attachments: [{ content: signedPdfBuffer, filename: documentName }],
});
return token;
} catch (err) {
console.error(err);
throw err;
}
}),
});

View File

@ -1,33 +0,0 @@
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { DocumentDataType, FieldType } from '@documenso/prisma/client';
export const ZCreateSinglePlayerDocumentMutationSchema = z.object({
documentData: z.object({
data: z.string(),
type: z.nativeEnum(DocumentDataType),
}),
documentName: z.string(),
signer: z.object({
email: z.string().email().min(1),
name: z.string(),
signature: z.string(),
customText: z.string(),
}),
fields: z.array(
z.object({
page: z.number(),
type: z.nativeEnum(FieldType),
positionX: z.number(),
positionY: z.number(),
width: z.number(),
height: z.number(),
}),
),
fieldMeta: ZFieldMetaSchema,
});
export type TCreateSinglePlayerDocumentMutationSchema = z.infer<
typeof ZCreateSinglePlayerDocumentMutationSchema
>;

File diff suppressed because it is too large Load Diff

View File

@ -2,17 +2,11 @@ import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { ZUpdatePublicProfileMutationSchema } from '../profile-router/schema';
// Consider refactoring to use ZBaseTableSearchParamsSchema.
const GenericFindQuerySchema = z.object({
term: z.string().optional(),
page: z.number().min(1).optional(),
perPage: z.number().min(1).optional(),
});
/**
* Restrict team URLs schema.
*
@ -122,17 +116,17 @@ export const ZFindTeamInvoicesQuerySchema = z.object({
teamId: z.number(),
});
export const ZFindTeamMemberInvitesQuerySchema = GenericFindQuerySchema.extend({
export const ZFindTeamMemberInvitesQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number(),
});
export const ZFindTeamMembersQuerySchema = GenericFindQuerySchema.extend({
export const ZFindTeamMembersQuerySchema = ZFindSearchParamsSchema.extend({
teamId: z.number(),
});
export const ZFindTeamsQuerySchema = GenericFindQuerySchema;
export const ZFindTeamsQuerySchema = ZFindSearchParamsSchema;
export const ZFindTeamsPendingQuerySchema = GenericFindQuerySchema;
export const ZFindTeamsPendingQuerySchema = ZFindSearchParamsSchema;
export const ZGetTeamQuerySchema = z.object({
teamId: z.number(),
@ -151,8 +145,6 @@ export const ZUpdateTeamMutationSchema = z.object({
data: z.object({
name: ZTeamNameSchema,
url: ZTeamUrlSchema,
documentVisibility: z.nativeEnum(DocumentVisibility).optional(),
includeSenderDetails: z.boolean().optional(),
}),
});
@ -212,6 +204,8 @@ export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
.default(DocumentVisibility.EVERYONE),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
includeSenderDetails: z.boolean().optional().default(false),
typedSignatureEnabled: z.boolean().optional().default(true),
includeSigningCertificate: z.boolean().optional().default(true),
}),
});

View File

@ -1,362 +1,465 @@
import { TRPCError } from '@trpc/server';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { jobs } from '@documenso/lib/jobs/client';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createDocumentFromDirectTemplate } from '@documenso/lib/server-only/template/create-document-from-direct-template';
import {
ZCreateDocumentFromDirectTemplateResponseSchema,
createDocumentFromDirectTemplate,
} from '@documenso/lib/server-only/template/create-document-from-direct-template';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import {
ZCreateTemplateResponseSchema,
createTemplate,
} from '@documenso/lib/server-only/template/create-template';
import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link';
import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { getTemplateWithDetailsById } from '@documenso/lib/server-only/template/get-template-with-details-by-id';
import { moveTemplateToTeam } from '@documenso/lib/server-only/template/move-template-to-team';
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
import { updateTemplateSettings } from '@documenso/lib/server-only/template/update-template-settings';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { updateTemplate } from '@documenso/lib/server-only/template/update-template';
import type { Document } from '@documenso/prisma/client';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
import {
ZCreateDocumentFromDirectTemplateMutationSchema,
ZCreateDocumentFromTemplateMutationSchema,
ZCreateTemplateDirectLinkMutationSchema,
ZBulkSendTemplateMutationSchema,
ZCreateDocumentFromDirectTemplateRequestSchema,
ZCreateDocumentFromTemplateRequestSchema,
ZCreateDocumentFromTemplateResponseSchema,
ZCreateTemplateDirectLinkRequestSchema,
ZCreateTemplateDirectLinkResponseSchema,
ZCreateTemplateMutationSchema,
ZDeleteTemplateDirectLinkMutationSchema,
ZDeleteTemplateDirectLinkRequestSchema,
ZDeleteTemplateMutationSchema,
ZDuplicateTemplateMutationSchema,
ZFindTemplatesQuerySchema,
ZGetTemplateWithDetailsByIdQuerySchema,
ZMoveTemplatesToTeamSchema,
ZSetSigningOrderForTemplateMutationSchema,
ZToggleTemplateDirectLinkMutationSchema,
ZUpdateTemplateSettingsMutationSchema,
ZDuplicateTemplateResponseSchema,
ZFindTemplatesRequestSchema,
ZFindTemplatesResponseSchema,
ZGetTemplateByIdRequestSchema,
ZGetTemplateByIdResponseSchema,
ZMoveTemplateToTeamRequestSchema,
ZMoveTemplateToTeamResponseSchema,
ZToggleTemplateDirectLinkRequestSchema,
ZToggleTemplateDirectLinkResponseSchema,
ZUpdateTemplateRequestSchema,
ZUpdateTemplateResponseSchema,
} from './schema';
export const templateRouter = router({
createTemplate: authenticatedProcedure
.input(ZCreateTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { teamId, title, templateDocumentDataId } = input;
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this template. Please try again later.',
});
}
}),
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
.input(ZCreateDocumentFromDirectTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const {
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
} = input;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
user: ctx.user
? {
id: ctx.user.id,
name: ctx.user.name || undefined,
email: ctx.user.email,
}
: undefined,
requestMetadata,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
}),
createDocumentFromTemplate: authenticatedProcedure
.input(ZCreateDocumentFromTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, recipients } = input;
const limits = await getServerLimits({ email: ctx.user.email, teamId });
if (limits.remaining.documents === 0) {
throw new Error('You have reached your document limit.');
}
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
let document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
recipients,
requestMetadata,
});
if (input.distributeDocument) {
document = await sendDocument({
documentId: document.id,
userId: ctx.user.id,
teamId,
requestMetadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
}
return document;
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
}),
duplicateTemplate: authenticatedProcedure
.input(ZDuplicateTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { teamId, templateId } = input;
return await duplicateTemplate({
userId: ctx.user.id,
teamId,
templateId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to duplicate the template. Please try again later.',
});
}
}),
deleteTemplate: authenticatedProcedure
.input(ZDeleteTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id, teamId } = input;
const userId = ctx.user.id;
return await deleteTemplate({ userId, id, teamId });
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to delete this template. Please try again later.',
});
}
}),
getTemplateWithDetailsById: authenticatedProcedure
.input(ZGetTemplateWithDetailsByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await getTemplateWithDetailsById({
id: input.id,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this template. Please try again later.',
});
}
}),
// Todo: Add API
updateTemplateSettings: authenticatedProcedure
.input(ZUpdateTemplateSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, data, meta } = input;
const userId = ctx.user.id;
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
return await updateTemplateSettings({
userId,
teamId,
templateId,
data,
meta: {
...meta,
language: isValidLanguageCode(meta?.language) ? meta?.language : undefined,
},
requestMetadata,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this template. Please try again later.',
});
}
}),
setSigningOrderForTemplate: authenticatedProcedure
.input(ZSetSigningOrderForTemplateMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, signingOrder } = input;
return await updateTemplateSettings({
templateId,
teamId,
data: {},
meta: { signingOrder },
userId: ctx.user.id,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this document. Please try again later.',
});
}
}),
/**
* @public
*/
findTemplates: authenticatedProcedure
.input(ZFindTemplatesQuerySchema)
.meta({
openapi: {
method: 'GET',
path: '/template',
summary: 'Find templates',
description: 'Find templates based on a search criteria',
tags: ['Template'],
},
})
.input(ZFindTemplatesRequestSchema)
.output(ZFindTemplatesResponseSchema)
.query(async ({ input, ctx }) => {
try {
return await findTemplates({
const { teamId } = ctx;
return await findTemplates({
userId: ctx.user.id,
teamId,
...input,
});
}),
/**
* @public
*/
getTemplateById: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/template/{templateId}',
summary: 'Get template',
tags: ['Template'],
},
})
.input(ZGetTemplateByIdRequestSchema)
.output(ZGetTemplateByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
return await getTemplateById({
id: templateId,
userId: ctx.user.id,
teamId,
});
}),
/**
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createTemplate: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/create',
// summary: 'Create template',
// description: 'Create a new template',
// tags: ['Template'],
// },
// })
.input(ZCreateTemplateMutationSchema)
.output(ZCreateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { title, templateDocumentDataId } = input;
return await createTemplate({
userId: ctx.user.id,
teamId,
title,
templateDocumentDataId,
});
}),
/**
* @public
*/
updateTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/update',
summary: 'Update template',
tags: ['Template'],
},
})
.input(ZUpdateTemplateRequestSchema)
.output(ZUpdateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, data, meta } = input;
const userId = ctx.user.id;
return await updateTemplate({
userId,
teamId,
templateId,
data,
meta,
});
}),
/**
* @public
*/
duplicateTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/duplicate',
summary: 'Duplicate template',
tags: ['Template'],
},
})
.input(ZDuplicateTemplateMutationSchema)
.output(ZDuplicateTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
return await duplicateTemplate({
userId: ctx.user.id,
teamId,
templateId,
});
}),
/**
* @public
*/
deleteTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/delete',
summary: 'Delete template',
tags: ['Template'],
},
})
.input(ZDeleteTemplateMutationSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
await deleteTemplate({ userId, id: templateId, teamId });
return ZGenericSuccessResponse;
}),
/**
* @public
*/
createDocumentFromTemplate: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/use',
summary: 'Use template',
description: 'Use the template to create a document',
tags: ['Template'],
},
})
.input(ZCreateDocumentFromTemplateRequestSchema)
.output(ZCreateDocumentFromTemplateResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId } = ctx;
const { templateId, recipients, distributeDocument, customDocumentDataId } = input;
const limits = await getServerLimits({ email: ctx.user.email, teamId });
if (limits.remaining.documents === 0) {
throw new Error('You have reached your document limit.');
}
const document: Document = await createDocumentFromTemplate({
templateId,
teamId,
userId: ctx.user.id,
recipients,
customDocumentDataId,
requestMetadata: ctx.metadata,
});
if (distributeDocument) {
await sendDocument({
documentId: document.id,
userId: ctx.user.id,
...input,
});
} catch (err) {
console.error(err);
throw AppError.parseErrorToTRPCError(err);
}
}),
createTemplateDirectLink: authenticatedProcedure
.input(ZCreateTemplateDirectLinkMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, directRecipientId } = input;
const userId = ctx.user.id;
const template = await getTemplateById({ id: templateId, teamId, userId: ctx.user.id });
const limits = await getServerLimits({ email: ctx.user.email, teamId: template.teamId });
if (limits.remaining.directTemplates === 0) {
throw new AppError(
AppErrorCode.LIMIT_EXCEEDED,
'You have reached your direct templates limit.',
);
}
return await createTemplateDirectLink({ userId, templateId, directRecipientId });
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
throw AppError.parseErrorToTRPCError(error);
}
}),
deleteTemplateDirectLink: authenticatedProcedure
.input(ZDeleteTemplateDirectLinkMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId } = input;
const userId = ctx.user.id;
return await deleteTemplateDirectLink({ userId, templateId });
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
throw AppError.parseErrorToTRPCError(error);
}
}),
toggleTemplateDirectLink: authenticatedProcedure
.input(ZToggleTemplateDirectLinkMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, enabled } = input;
const userId = ctx.user.id;
return await toggleTemplateDirectLink({ userId, templateId, enabled });
} catch (err) {
console.error(err);
const error = AppError.parseError(err);
throw AppError.parseErrorToTRPCError(error);
}
}),
moveTemplateToTeam: authenticatedProcedure
.input(ZMoveTemplatesToTeamSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId } = input;
const userId = ctx.user.id;
return await moveTemplateToTeam({
templateId,
teamId,
userId,
requestMetadata: ctx.metadata,
}).catch((err) => {
console.error(err);
throw new AppError('DOCUMENT_SEND_FAILED');
});
} catch (err) {
console.error(err);
}
if (err instanceof TRPCError) {
throw err;
}
return getDocumentWithDetailsById({
documentId: document.id,
userId: ctx.user.id,
teamId,
});
}),
/**
* Leaving this endpoint as private for now until there is a use case for it.
*
* @private
*/
createDocumentFromDirectTemplate: maybeAuthenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/template/direct/use',
// summary: 'Use direct template',
// description: 'Use a direct template to create a document',
// tags: ['Template'],
// },
// })
.input(ZCreateDocumentFromDirectTemplateRequestSchema)
.output(ZCreateDocumentFromDirectTemplateResponseSchema)
.mutation(async ({ input, ctx }) => {
const {
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
} = input;
return await createDocumentFromDirectTemplate({
directRecipientName,
directRecipientEmail,
directTemplateToken,
directTemplateExternalId,
signedFieldValues,
templateUpdatedAt,
user: ctx.user
? {
id: ctx.user.id,
name: ctx.user.name || undefined,
email: ctx.user.email,
}
: undefined,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*/
createTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/direct/create',
summary: 'Create direct link',
description: 'Create a direct link for a template',
tags: ['Template'],
},
})
.input(ZCreateTemplateDirectLinkRequestSchema)
.output(ZCreateTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, directRecipientId } = input;
const userId = ctx.user.id;
const template = await getTemplateById({ id: templateId, teamId, userId: ctx.user.id });
const limits = await getServerLimits({ email: ctx.user.email, teamId: template.teamId });
if (limits.remaining.directTemplates === 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your direct templates limit.',
});
}
return await createTemplateDirectLink({ userId, teamId, templateId, directRecipientId });
}),
/**
* @public
*/
deleteTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/direct/delete',
summary: 'Delete direct link',
description: 'Delete a direct link for a template',
tags: ['Template'],
},
})
.input(ZDeleteTemplateDirectLinkRequestSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId } = input;
const userId = ctx.user.id;
await deleteTemplateDirectLink({ userId, teamId, templateId });
return ZGenericSuccessResponse;
}),
/**
* @public
*/
toggleTemplateDirectLink: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/direct/toggle',
summary: 'Toggle direct link',
description: 'Enable or disable a direct link for a template',
tags: ['Template'],
},
})
.input(ZToggleTemplateDirectLinkRequestSchema)
.output(ZToggleTemplateDirectLinkResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { templateId, enabled } = input;
const userId = ctx.user.id;
return await toggleTemplateDirectLink({ userId, teamId, templateId, enabled });
}),
/**
* @public
*/
moveTemplateToTeam: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/template/move',
summary: 'Move template',
description: 'Move a template to a team',
tags: ['Template'],
},
})
.input(ZMoveTemplateToTeamRequestSchema)
.output(ZMoveTemplateToTeamResponseSchema)
.mutation(async ({ input, ctx }) => {
const { templateId, teamId } = input;
const userId = ctx.user.id;
return await moveTemplateToTeam({
templateId,
teamId,
userId,
});
}),
/**
* @private
*/
uploadBulkSend: authenticatedProcedure
.input(ZBulkSendTemplateMutationSchema)
.mutation(async ({ ctx, input }) => {
const { templateId, teamId, csv, sendImmediately } = input;
const { user } = ctx;
if (csv.length > 4 * 1024 * 1024) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to move this template. Please try again later.',
message: 'File size exceeds 4MB limit',
});
}
const template = await getTemplateById({
id: templateId,
teamId,
userId: user.id,
});
if (!template) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Template not found',
});
}
await jobs.triggerJob({
name: 'internal.bulk-send-template',
payload: {
userId: user.id,
teamId,
templateId,
csvContent: csv,
sendImmediately,
requestMetadata: ctx.metadata.requestMetadata,
},
});
return { success: true };
}),
});

View File

@ -1,28 +1,38 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { ZDocumentSchema } from '@documenso/lib/types/document';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { ZBaseTableSearchParamsSchema } from '@documenso/lib/types/search-params';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import {
DocumentDistributionMethod,
DocumentSigningOrder,
TemplateType,
} from '@documenso/prisma/client';
ZTemplateLiteSchema,
ZTemplateManySchema,
ZTemplateSchema,
} from '@documenso/lib/types/template';
import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@documenso/prisma/client';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
export const ZCreateTemplateMutationSchema = z.object({
title: z.string().min(1).trim(),
teamId: z.number().optional(),
templateDocumentDataId: z.string().min(1),
});
export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
directRecipientName: z.string().optional(),
directRecipientEmail: z.string().email(),
directTemplateToken: z.string().min(1),
@ -31,120 +41,161 @@ export const ZCreateDocumentFromDirectTemplateMutationSchema = z.object({
templateUpdatedAt: z.date(),
});
export const ZCreateDocumentFromTemplateMutationSchema = z.object({
export const ZCreateDocumentFromTemplateRequestSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
recipients: z
.array(
z.object({
id: z.number(),
id: z.number().describe('The ID of the recipient in the template.'),
email: z.string().email(),
name: z.string().optional(),
}),
)
.describe('The information of the recipients to create the document with.')
.refine((recipients) => {
const emails = recipients.map((signer) => signer.email);
return new Set(emails).size === emails.length;
}, 'Recipients must have unique emails'),
distributeDocument: z.boolean().optional(),
distributeDocument: z
.boolean()
.describe('Whether to create the document as pending and distribute it to recipients.')
.optional(),
customDocumentDataId: z
.string()
.describe(
'The data ID of an alternative PDF to use when creating the document. If not provided, the PDF attached to the template will be used.',
)
.optional(),
});
export const ZCreateDocumentFromTemplateResponseSchema = ZDocumentSchema;
export const ZDuplicateTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
});
export const ZCreateTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
teamId: z.number().optional(),
directRecipientId: z.number().min(1).optional(),
export const ZDuplicateTemplateResponseSchema = ZTemplateLiteSchema;
export const ZCreateTemplateDirectLinkRequestSchema = z.object({
templateId: z.number(),
directRecipientId: z
.number()
.describe(
'The of the recipient in the current template to transform into the primary recipient when the template is used.',
)
.optional(),
});
export const ZDeleteTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
const GenericDirectLinkResponseSchema = TemplateDirectLinkSchema.pick({
id: true,
templateId: true,
token: true,
createdAt: true,
enabled: true,
directTemplateRecipientId: true,
});
export const ZToggleTemplateDirectLinkMutationSchema = z.object({
templateId: z.number().min(1),
export const ZCreateTemplateDirectLinkResponseSchema = GenericDirectLinkResponseSchema;
export const ZDeleteTemplateDirectLinkRequestSchema = z.object({
templateId: z.number(),
});
export const ZToggleTemplateDirectLinkRequestSchema = z.object({
templateId: z.number(),
enabled: z.boolean(),
});
export const ZToggleTemplateDirectLinkResponseSchema = GenericDirectLinkResponseSchema;
export const ZDeleteTemplateMutationSchema = z.object({
id: z.number().min(1),
teamId: z.number().optional(),
templateId: z.number(),
});
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
export const MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH = 256;
export const ZUpdateTemplateSettingsMutationSchema = z.object({
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
teamId: z.number().min(1).optional(),
data: z.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z.string().trim().min(1).max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH).optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.optional(),
type: z.nativeEnum(TemplateType).optional(),
language: z
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
}),
data: z
.object({
title: z.string().min(1).optional(),
externalId: z.string().nullish(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable().optional(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable().optional(),
publicTitle: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_TITLE_LENGTH)
.describe(
'The title of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
publicDescription: z
.string()
.trim()
.min(1)
.max(MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH)
.describe(
'The description of the template that will be displayed to the public. Only applicable for public templates.',
)
.optional(),
type: z.nativeEnum(TemplateType).optional(),
})
.optional(),
meta: z
.object({
subject: z.string(),
message: z.string(),
timezone: z.string(),
dateFormat: z.string(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
emailSettings: ZDocumentEmailSettingsSchema,
redirectUrl: z
.string()
.optional()
.refine((value) => value === undefined || value === '' || isValidRedirectUrl(value), {
message:
'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
})
.optional(),
});
export const ZSetSigningOrderForTemplateMutationSchema = z.object({
export const ZUpdateTemplateResponseSchema = ZTemplateLiteSchema;
export const ZFindTemplatesRequestSchema = ZFindSearchParamsSchema.extend({
type: z.nativeEnum(TemplateType).describe('Filter templates by type.').optional(),
});
export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({
data: ZTemplateManySchema.array(),
});
export type TFindTemplatesResponse = z.infer<typeof ZFindTemplatesResponseSchema>;
export type FindTemplateRow = TFindTemplatesResponse['data'][number];
export const ZGetTemplateByIdRequestSchema = z.object({
templateId: z.number(),
});
export const ZGetTemplateByIdResponseSchema = ZTemplateSchema;
export const ZMoveTemplateToTeamRequestSchema = z.object({
templateId: z.number().describe('The ID of the template to move to.'),
teamId: z.number().describe('The ID of the team to move the template to.'),
});
export const ZMoveTemplateToTeamResponseSchema = ZTemplateLiteSchema;
export const ZBulkSendTemplateMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder),
});
export const ZFindTemplatesQuerySchema = ZBaseTableSearchParamsSchema.extend({
teamId: z.number().optional(),
type: z.nativeEnum(TemplateType).optional(),
});
export const ZGetTemplateWithDetailsByIdQuerySchema = z.object({
id: z.number().min(1),
});
export const ZMoveTemplatesToTeamSchema = z.object({
templateId: z.number(),
teamId: z.number(),
csv: z.string().min(1),
sendImmediately: z.boolean(),
});
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
export type TCreateDocumentFromTemplateMutationSchema = z.infer<
typeof ZCreateDocumentFromTemplateMutationSchema
>;
export type TDuplicateTemplateMutationSchema = z.infer<typeof ZDuplicateTemplateMutationSchema>;
export type TDeleteTemplateMutationSchema = z.infer<typeof ZDeleteTemplateMutationSchema>;
export type TGetTemplateWithDetailsByIdQuerySchema = z.infer<
typeof ZGetTemplateWithDetailsByIdQuerySchema
>;
export type TMoveTemplatesToSchema = z.infer<typeof ZMoveTemplatesToTeamSchema>;
export type TBulkSendTemplateMutationSchema = z.infer<typeof ZBulkSendTemplateMutationSchema>;

View File

@ -1,22 +1,114 @@
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 { isAdmin } from '@documenso/lib/next-auth/guards/is-admin';
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 { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import type { TrpcContext } from './context';
const t = initTRPC.context<TrpcContext>().create({
transformer: SuperJSON,
});
// Can't import type from trpc-to-openapi because it breaks nextjs build, not sure why.
type OpenApiMeta = {
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<OpenApiMeta>()
.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.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: 'You must be logged in to perform this action.',
message: 'Invalid session or API token.',
});
}
@ -25,16 +117,39 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => {
...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 }) => {
const requestMetadata = extractNextApiRequestMetadata(ctx.req);
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,
requestMetadata,
auth: ctx.session ? 'session' : null,
} satisfies ApiRequestMetadata,
},
});
});
@ -61,6 +176,15 @@ export const adminMiddleware = t.middleware(async ({ ctx, next }) => {
...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,
},
});
});

View File

@ -1,11 +1,7 @@
import { TRPCError } from '@trpc/server';
import { AppError } from '@documenso/lib/errors/app-error';
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
import { enableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/enable-2fa';
import { setupTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/setup-2fa';
import { viewBackupCodes } from '@documenso/lib/server-only/2fa/view-backup-codes';
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { authenticatedProcedure, router } from '../trpc';
import {
@ -16,89 +12,44 @@ import {
export const twoFactorAuthenticationRouter = router({
setup: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
return await setupTwoFactorAuthentication({
user: ctx.user,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to setup two-factor authentication. Please try again later.',
});
}
return await setupTwoFactorAuthentication({
user: ctx.user,
});
}),
enable: authenticatedProcedure
.input(ZEnableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const user = ctx.user;
const { code } = input;
const { code } = input;
return await enableTwoFactorAuthentication({
user,
code,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to enable two-factor authentication. Please try again later.',
});
}
return await enableTwoFactorAuthentication({
user,
code,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
disable: authenticatedProcedure
.input(ZDisableTwoFactorAuthenticationMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const user = ctx.user;
const user = ctx.user;
return await disableTwoFactorAuthentication({
user,
totpCode: input.totpCode,
backupCode: input.backupCode,
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to disable two-factor authentication. Please try again later.',
});
}
return await disableTwoFactorAuthentication({
user,
totpCode: input.totpCode,
backupCode: input.backupCode,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
viewRecoveryCodes: authenticatedProcedure
.input(ZViewRecoveryCodesMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
return await viewBackupCodes({
user: ctx.user,
token: input.token,
});
} catch (err) {
const error = AppError.parseError(err);
if (error.code !== 'INCORRECT_TWO_FACTOR_CODE') {
console.error(err);
}
throw AppError.parseErrorToTRPCError(err);
}
return await viewBackupCodes({
user: ctx.user,
token: input.token,
});
}),
});

View File

@ -1,5 +1,3 @@
import { TRPCError } from '@trpc/server';
import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook';
import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id';
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
@ -18,16 +16,7 @@ import {
export const webhookRouter = router({
getWebhooks: authenticatedProcedure.query(async ({ ctx }) => {
try {
return await getWebhooksByUserId(ctx.user.id);
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to fetch your webhooks. Please try again later.',
});
}
return await getWebhooksByUserId(ctx.user.id);
}),
getTeamWebhooks: authenticatedProcedure
@ -35,37 +24,19 @@ export const webhookRouter = router({
.query(async ({ ctx, input }) => {
const { teamId } = input;
try {
return await getWebhooksByTeamId(teamId, ctx.user.id);
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to fetch your webhooks. Please try again later.',
});
}
return await getWebhooksByTeamId(teamId, ctx.user.id);
}),
getWebhookById: authenticatedProcedure
.input(ZGetWebhookByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
const { id, teamId } = input;
const { id, teamId } = input;
return await getWebhookById({
id,
userId: ctx.user.id,
teamId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to fetch your webhook. Please try again later.',
});
}
return await getWebhookById({
id,
userId: ctx.user.id,
teamId,
});
}),
createWebhook: authenticatedProcedure
@ -73,65 +44,38 @@ export const webhookRouter = router({
.mutation(async ({ input, ctx }) => {
const { enabled, eventTriggers, secret, webhookUrl, teamId } = input;
try {
return await createWebhook({
enabled,
secret,
webhookUrl,
eventTriggers,
teamId,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this webhook. Please try again later.',
});
}
return await createWebhook({
enabled,
secret,
webhookUrl,
eventTriggers,
teamId,
userId: ctx.user.id,
});
}),
deleteWebhook: authenticatedProcedure
.input(ZDeleteWebhookMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id, teamId } = input;
const { id, teamId } = input;
return await deleteWebhookById({
id,
teamId,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this webhook. Please try again later.',
});
}
return await deleteWebhookById({
id,
teamId,
userId: ctx.user.id,
});
}),
editWebhook: authenticatedProcedure
.input(ZEditWebhookMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { id, teamId, ...data } = input;
const { id, teamId, ...data } = input;
return await editWebhook({
id,
data,
userId: ctx.user.id,
teamId,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to create this webhook. Please try again later.',
});
}
return await editWebhook({
id,
data,
userId: ctx.user.id,
teamId,
});
}),
});