fix: merge conflicts

This commit is contained in:
Ephraim Atta-Duncan
2025-08-27 12:05:21 +00:00
185 changed files with 3075 additions and 1662 deletions

View File

@ -0,0 +1,28 @@
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document';
import { adminProcedure } from '../trpc';
import {
ZDeleteDocumentRequestSchema,
ZDeleteDocumentResponseSchema,
} from './delete-document.types';
export const deleteDocumentRoute = adminProcedure
.input(ZDeleteDocumentRequestSchema)
.output(ZDeleteDocumentResponseSchema)
.mutation(async ({ ctx, input }) => {
const { id, reason } = input;
ctx.logger.info({
input: {
id,
},
});
await sendDeleteEmail({ documentId: id, reason });
await superDeleteDocument({
id,
requestMetadata: ctx.metadata.requestMetadata,
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZDeleteDocumentRequestSchema = z.object({
id: z.number().min(1),
reason: z.string(),
});
export const ZDeleteDocumentResponseSchema = z.void();
export type TDeleteDocumentRequest = z.infer<typeof ZDeleteDocumentRequestSchema>;
export type TDeleteDocumentResponse = z.infer<typeof ZDeleteDocumentResponseSchema>;

View File

@ -0,0 +1,19 @@
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { adminProcedure } from '../trpc';
import { ZDeleteUserRequestSchema, ZDeleteUserResponseSchema } from './delete-user.types';
export const deleteUserRoute = adminProcedure
.input(ZDeleteUserRequestSchema)
.output(ZDeleteUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
await deleteUser({ id });
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteUserRequestSchema = z.object({
id: z.number().min(1),
});
export const ZDeleteUserResponseSchema = z.void();
export type TDeleteUserRequest = z.infer<typeof ZDeleteUserRequestSchema>;
export type TDeleteUserResponse = z.infer<typeof ZDeleteUserResponseSchema>;

View File

@ -0,0 +1,29 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { disableUser } from '@documenso/lib/server-only/user/disable-user';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { adminProcedure } from '../trpc';
import { ZDisableUserRequestSchema, ZDisableUserResponseSchema } from './disable-user.types';
export const disableUserRoute = adminProcedure
.input(ZDisableUserRequestSchema)
.output(ZDisableUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
await disableUser({ id });
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDisableUserRequestSchema = z.object({
id: z.number().min(1),
});
export const ZDisableUserResponseSchema = z.void();
export type TDisableUserRequest = z.infer<typeof ZDisableUserRequestSchema>;
export type TDisableUserResponse = z.infer<typeof ZDisableUserResponseSchema>;

View File

@ -0,0 +1,29 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { enableUser } from '@documenso/lib/server-only/user/enable-user';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { adminProcedure } from '../trpc';
import { ZEnableUserRequestSchema, ZEnableUserResponseSchema } from './enable-user.types';
export const enableUserRoute = adminProcedure
.input(ZEnableUserRequestSchema)
.output(ZEnableUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
await enableUser({ id });
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZEnableUserRequestSchema = z.object({
id: z.number().min(1),
});
export const ZEnableUserResponseSchema = z.void();
export type TEnableUserRequest = z.infer<typeof ZEnableUserRequestSchema>;
export type TEnableUserResponse = z.infer<typeof ZEnableUserResponseSchema>;

View File

@ -0,0 +1,13 @@
import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
import { adminProcedure } from '../trpc';
import { ZFindDocumentsRequestSchema, ZFindDocumentsResponseSchema } from './find-documents.types';
export const findDocumentsRoute = adminProcedure
.input(ZFindDocumentsRequestSchema)
.output(ZFindDocumentsResponseSchema)
.query(async ({ input }) => {
const { query, page, perPage } = input;
return await findDocuments({ query, page, perPage });
});

View File

@ -0,0 +1,17 @@
import { z } from 'zod';
import { ZDocumentManySchema } from '@documenso/lib/types/document';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
perPage: z.number().optional().default(20),
});
export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
data: ZDocumentManySchema.omit({
team: true,
}).array(),
});
export type TFindDocumentsRequest = z.infer<typeof ZFindDocumentsRequestSchema>;
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;

View File

@ -0,0 +1,19 @@
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { adminProcedure } from '../trpc';
import { ZGetUserRequestSchema, ZGetUserResponseSchema } from './get-user.types';
export const getUserRoute = adminProcedure
.input(ZGetUserRequestSchema)
.output(ZGetUserResponseSchema)
.query(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
return await getUserById({ id });
});

View File

@ -0,0 +1,21 @@
import { z } from 'zod';
import UserSchema from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
export const ZGetUserRequestSchema = z.object({
id: z.number().min(1),
});
export const ZGetUserResponseSchema = UserSchema.pick({
id: true,
name: true,
email: true,
emailVerified: true,
roles: true,
disabled: true,
twoFactorEnabled: true,
signature: true,
});
export type TGetUserRequest = z.infer<typeof ZGetUserRequestSchema>;
export type TGetUserResponse = z.infer<typeof ZGetUserResponseSchema>;

View File

@ -0,0 +1,28 @@
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { adminProcedure } from '../trpc';
import {
ZResealDocumentRequestSchema,
ZResealDocumentResponseSchema,
} from './reseal-document.types';
export const resealDocumentRoute = adminProcedure
.input(ZResealDocumentRequestSchema)
.output(ZResealDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const document = await getEntireDocument({ id });
const isResealing = isDocumentCompleted(document.status);
await sealDocument({ documentId: id, isResealing });
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZResealDocumentRequestSchema = z.object({
id: z.number().min(1),
});
export const ZResealDocumentResponseSchema = z.void();
export type TResealDocumentRequest = z.infer<typeof ZResealDocumentRequestSchema>;
export type TResealDocumentResponse = z.infer<typeof ZResealDocumentResponseSchema>;

View File

@ -0,0 +1,50 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { adminProcedure } from '../trpc';
import {
ZResetTwoFactorRequestSchema,
ZResetTwoFactorResponseSchema,
} from './reset-two-factor-authentication.types';
export const resetTwoFactorRoute = adminProcedure
.input(ZResetTwoFactorRequestSchema)
.output(ZResetTwoFactorResponseSchema)
.mutation(async ({ input, ctx }) => {
const { userId } = input;
ctx.logger.info({
input: {
userId,
},
});
return await resetTwoFactor({ userId });
});
export type ResetTwoFactorOptions = {
userId: number;
};
export const resetTwoFactor = async ({ userId }: ResetTwoFactorOptions) => {
const user = await prisma.user.findFirst({
where: {
id: userId,
},
});
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, { message: 'User not found' });
}
await prisma.user.update({
where: {
id: user.id,
},
data: {
twoFactorEnabled: false,
twoFactorBackupCodes: null,
twoFactorSecret: null,
},
});
};

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZResetTwoFactorRequestSchema = z.object({
userId: z.number(),
});
export const ZResetTwoFactorResponseSchema = z.void();
export type TResetTwoFactorRequest = z.infer<typeof ZResetTwoFactorRequestSchema>;
export type TResetTwoFactorResponse = z.infer<typeof ZResetTwoFactorResponseSchema>;

View File

@ -1,39 +1,24 @@
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';
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email';
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 { isDocumentCompleted } from '@documenso/lib/utils/document';
import { adminProcedure, router } from '../trpc';
import { router } from '../trpc';
import { createAdminOrganisationRoute } from './create-admin-organisation';
import { createStripeCustomerRoute } from './create-stripe-customer';
import { createSubscriptionClaimRoute } from './create-subscription-claim';
import { deleteDocumentRoute } from './delete-document';
import { deleteSubscriptionClaimRoute } from './delete-subscription-claim';
import { deleteUserRoute } from './delete-user';
import { disableUserRoute } from './disable-user';
import { enableUserRoute } from './enable-user';
import { findAdminOrganisationsRoute } from './find-admin-organisations';
import { findDocumentsRoute } from './find-documents';
import { findSubscriptionClaimsRoute } from './find-subscription-claims';
import { getAdminOrganisationRoute } from './get-admin-organisation';
import {
ZAdminDeleteDocumentMutationSchema,
ZAdminDeleteUserMutationSchema,
ZAdminDisableUserMutationSchema,
ZAdminEnableUserMutationSchema,
ZAdminFindDocumentsQuerySchema,
ZAdminResealDocumentMutationSchema,
ZAdminUpdateProfileMutationSchema,
ZAdminUpdateRecipientMutationSchema,
ZAdminUpdateSiteSettingMutationSchema,
} from './schema';
import { getUserRoute } from './get-user';
import { resealDocumentRoute } from './reseal-document';
import { resetTwoFactorRoute } from './reset-two-factor-authentication';
import { updateAdminOrganisationRoute } from './update-admin-organisation';
import { updateRecipientRoute } from './update-recipient';
import { updateSiteSettingRoute } from './update-site-setting';
import { updateSubscriptionClaimRoute } from './update-subscription-claim';
import { updateUserRoute } from './update-user';
export const adminRouter = router({
organisation: {
@ -51,154 +36,21 @@ export const adminRouter = router({
stripe: {
createCustomer: createStripeCustomerRoute,
},
// Todo: migrate old routes
findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
const { query, page, perPage } = input;
return await findDocuments({ query, page, perPage });
}),
updateUser: adminProcedure
.input(ZAdminUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id, name, email, roles } = input;
ctx.logger.info({
input: {
id,
roles,
},
});
return await updateUser({ id, name, email, roles });
}),
updateRecipient: adminProcedure
.input(ZAdminUpdateRecipientMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id, name, email } = input;
ctx.logger.info({
input: {
id,
},
});
return await updateRecipient({ id, name, email });
}),
updateSiteSetting: adminProcedure
.input(ZAdminUpdateSiteSettingMutationSchema)
.mutation(async ({ ctx, input }) => {
const { id, enabled, data } = input;
ctx.logger.info({
input: {
id,
},
});
return await upsertSiteSetting({
id,
enabled,
data,
userId: ctx.user.id,
});
}),
resealDocument: adminProcedure
.input(ZAdminResealDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const document = await getEntireDocument({ id });
const isResealing = isDocumentCompleted(document.status);
return await sealDocument({ documentId: id, isResealing });
}),
enableUser: adminProcedure
.input(ZAdminEnableUserMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
const user = await getUserById({ id }).catch(() => null);
if (!user) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'User not found',
});
}
return await enableUser({ id });
}),
disableUser: adminProcedure
.input(ZAdminDisableUserMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
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, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
return await deleteUser({ id });
}),
deleteDocument: adminProcedure
.input(ZAdminDeleteDocumentMutationSchema)
.mutation(async ({ ctx, input }) => {
const { id, reason } = input;
ctx.logger.info({
input: {
id,
},
});
await sendDeleteEmail({ documentId: id, reason });
return await superDeleteDocument({
id,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
user: {
get: getUserRoute,
update: updateUserRoute,
delete: deleteUserRoute,
enable: enableUserRoute,
disable: disableUserRoute,
resetTwoFactor: resetTwoFactorRoute,
},
document: {
find: findDocumentsRoute,
delete: deleteDocumentRoute,
reseal: resealDocumentRoute,
},
recipient: {
update: updateRecipientRoute,
},
updateSiteSetting: updateSiteSettingRoute,
});

View File

@ -1,67 +0,0 @@
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 = ZFindSearchParamsSchema.extend({
perPage: z.number().optional().default(20),
});
export type TAdminFindDocumentsQuerySchema = z.infer<typeof ZAdminFindDocumentsQuerySchema>;
export const ZAdminUpdateProfileMutationSchema = z.object({
id: z.number().min(1),
name: z.string().nullish(),
email: z.string().email().optional(),
roles: z.array(z.nativeEnum(Role)).optional(),
});
export type TAdminUpdateProfileMutationSchema = z.infer<typeof ZAdminUpdateProfileMutationSchema>;
export const ZAdminUpdateRecipientMutationSchema = z.object({
id: z.number().min(1),
name: z.string().optional(),
email: z.string().email().optional(),
});
export type TAdminUpdateRecipientMutationSchema = z.infer<
typeof ZAdminUpdateRecipientMutationSchema
>;
export const ZAdminUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
export type TAdminUpdateSiteSettingMutationSchema = z.infer<
typeof ZAdminUpdateSiteSettingMutationSchema
>;
export const ZAdminResealDocumentMutationSchema = z.object({
id: z.number().min(1),
});
export type TAdminResealDocumentMutationSchema = z.infer<typeof ZAdminResealDocumentMutationSchema>;
export const ZAdminDeleteUserMutationSchema = z.object({
id: z.number().min(1),
});
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(),
});
export type TAdminDeleteDocomentMutationSchema = z.infer<typeof ZAdminDeleteDocumentMutationSchema>;

View File

@ -0,0 +1,22 @@
import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
import { adminProcedure } from '../trpc';
import {
ZUpdateRecipientRequestSchema,
ZUpdateRecipientResponseSchema,
} from './update-recipient.types';
export const updateRecipientRoute = adminProcedure
.input(ZUpdateRecipientRequestSchema)
.output(ZUpdateRecipientResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id, name, email } = input;
ctx.logger.info({
input: {
id,
},
});
await updateRecipient({ id, name, email });
});

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const ZUpdateRecipientRequestSchema = z.object({
id: z.number().min(1),
name: z.string().optional(),
email: z.string().email().optional(),
});
export const ZUpdateRecipientResponseSchema = z.void();
export type TUpdateRecipientRequest = z.infer<typeof ZUpdateRecipientRequestSchema>;
export type TUpdateRecipientResponse = z.infer<typeof ZUpdateRecipientResponseSchema>;

View File

@ -0,0 +1,27 @@
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
import { adminProcedure } from '../trpc';
import {
ZUpdateSiteSettingRequestSchema,
ZUpdateSiteSettingResponseSchema,
} from './update-site-setting.types';
export const updateSiteSettingRoute = adminProcedure
.input(ZUpdateSiteSettingRequestSchema)
.output(ZUpdateSiteSettingResponseSchema)
.mutation(async ({ ctx, input }) => {
const { id, enabled, data } = input;
ctx.logger.info({
input: {
id,
},
});
await upsertSiteSetting({
id,
enabled,
data,
userId: ctx.user.id,
});
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
export const ZUpdateSiteSettingRequestSchema = ZSiteSettingSchema;
export const ZUpdateSiteSettingResponseSchema = z.void();
export type TUpdateSiteSettingRequest = z.infer<typeof ZUpdateSiteSettingRequestSchema>;
export type TUpdateSiteSettingResponse = z.infer<typeof ZUpdateSiteSettingResponseSchema>;

View File

@ -0,0 +1,20 @@
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
import { adminProcedure } from '../trpc';
import { ZUpdateUserRequestSchema, ZUpdateUserResponseSchema } from './update-user.types';
export const updateUserRoute = adminProcedure
.input(ZUpdateUserRequestSchema)
.output(ZUpdateUserResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id, name, email, roles } = input;
ctx.logger.info({
input: {
id,
roles,
},
});
await updateUser({ id, name, email, roles });
});

View File

@ -0,0 +1,14 @@
import { Role } from '@prisma/client';
import { z } from 'zod';
export const ZUpdateUserRequestSchema = z.object({
id: z.number().min(1),
name: z.string().nullish(),
email: z.string().email().optional(),
roles: z.array(z.nativeEnum(Role)).optional(),
});
export const ZUpdateUserResponseSchema = z.void();
export type TUpdateUserRequest = z.infer<typeof ZUpdateUserRequestSchema>;
export type TUpdateUserResponse = z.infer<typeof ZUpdateUserResponseSchema>;

View File

@ -0,0 +1,27 @@
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateApiTokenRequestSchema,
ZCreateApiTokenResponseSchema,
} from './create-api-token.types';
export const createApiTokenRoute = authenticatedProcedure
.input(ZCreateApiTokenRequestSchema)
.output(ZCreateApiTokenResponseSchema)
.mutation(async ({ input, ctx }) => {
const { tokenName, teamId, expirationDate } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
expiresIn: expirationDate,
});
});

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const ZCreateApiTokenRequestSchema = z.object({
teamId: z.number(),
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
expirationDate: z.string().nullable(),
});
export const ZCreateApiTokenResponseSchema = z.object({
id: z.number(),
token: z.string(),
});

View File

@ -0,0 +1,27 @@
import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteApiTokenRequestSchema,
ZDeleteApiTokenResponseSchema,
} from './delete-api-token.types';
export const deleteApiTokenRoute = authenticatedProcedure
.input(ZDeleteApiTokenRequestSchema)
.output(ZDeleteApiTokenResponseSchema)
.mutation(async ({ input, ctx }) => {
const { id, teamId } = input;
ctx.logger.info({
input: {
id,
teamId,
},
});
await deleteTokenById({
id,
teamId,
userId: ctx.user.id,
});
});

View File

@ -0,0 +1,8 @@
import { z } from 'zod';
export const ZDeleteApiTokenRequestSchema = z.object({
id: z.number().min(1),
teamId: z.number(),
});
export const ZDeleteApiTokenResponseSchema = z.void();

View File

@ -0,0 +1,19 @@
import { getApiTokens } from '@documenso/lib/server-only/public-api/get-api-tokens';
import { authenticatedProcedure } from '../trpc';
import { ZGetApiTokensRequestSchema, ZGetApiTokensResponseSchema } from './get-api-tokens.types';
export const getApiTokensRoute = authenticatedProcedure
.input(ZGetApiTokensRequestSchema)
.output(ZGetApiTokensResponseSchema)
.query(async ({ ctx }) => {
const { teamId } = ctx;
ctx.logger.info({
input: {
teamId,
},
});
return await getApiTokens({ userId: ctx.user.id, teamId });
});

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
import ApiTokenSchema from '@documenso/prisma/generated/zod/modelSchema/ApiTokenSchema';
export const ZGetApiTokensRequestSchema = z.void();
export const ZGetApiTokensResponseSchema = z.array(
ApiTokenSchema.pick({
id: true,
name: true,
createdAt: true,
expires: true,
}),
);
export type TGetApiTokensResponse = z.infer<typeof ZGetApiTokensResponseSchema>;

View File

@ -1,50 +1,10 @@
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 { getApiTokens } from '@documenso/lib/server-only/public-api/get-api-tokens';
import { authenticatedProcedure, router } from '../trpc';
import { ZCreateTokenMutationSchema, ZDeleteTokenByIdMutationSchema } from './schema';
import { router } from '../trpc';
import { createApiTokenRoute } from './create-api-token';
import { deleteApiTokenRoute } from './delete-api-token';
import { getApiTokensRoute } from './get-api-tokens';
export const apiTokenRouter = router({
getTokens: authenticatedProcedure.query(async ({ ctx }) => {
return await getApiTokens({ userId: ctx.user.id, teamId: ctx.teamId });
}),
createToken: authenticatedProcedure
.input(ZCreateTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
const { tokenName, teamId, expirationDate } = input;
ctx.logger.info({
input: {
teamId,
},
});
return await createApiToken({
userId: ctx.user.id,
teamId,
tokenName,
expiresIn: expirationDate,
});
}),
deleteTokenById: authenticatedProcedure
.input(ZDeleteTokenByIdMutationSchema)
.mutation(async ({ input, ctx }) => {
const { id, teamId } = input;
ctx.logger.info({
input: {
id,
teamId,
},
});
return await deleteTokenById({
id,
teamId,
userId: ctx.user.id,
});
}),
create: createApiTokenRoute,
getMany: getApiTokensRoute,
delete: deleteApiTokenRoute,
});

View File

@ -1,16 +0,0 @@
import { z } from 'zod';
export const ZCreateTokenMutationSchema = z.object({
teamId: z.number(),
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
expirationDate: z.string().nullable(),
});
export type TCreateTokenMutationSchema = z.infer<typeof ZCreateTokenMutationSchema>;
export const ZDeleteTokenByIdMutationSchema = z.object({
id: z.number().min(1),
teamId: z.number(),
});
export type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenByIdMutationSchema>;

View File

@ -0,0 +1,17 @@
import { createPasskeyAuthenticationOptions } from '@documenso/lib/server-only/auth/create-passkey-authentication-options';
import { authenticatedProcedure } from '../trpc';
import {
ZCreatePasskeyAuthenticationOptionsRequestSchema,
ZCreatePasskeyAuthenticationOptionsResponseSchema,
} from './create-passkey-authentication-options.types';
export const createPasskeyAuthenticationOptionsRoute = authenticatedProcedure
.input(ZCreatePasskeyAuthenticationOptionsRequestSchema)
.output(ZCreatePasskeyAuthenticationOptionsResponseSchema)
.mutation(async ({ ctx, input }) => {
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
preferredPasskeyId: input?.preferredPasskeyId,
});
});

View File

@ -0,0 +1,19 @@
import { z } from 'zod';
export const ZCreatePasskeyAuthenticationOptionsRequestSchema = z
.object({
preferredPasskeyId: z.string().optional(),
})
.optional();
export const ZCreatePasskeyAuthenticationOptionsResponseSchema = z.object({
tokenReference: z.string(),
options: z.any(), // PublicKeyCredentialRequestOptions type
});
export type TCreatePasskeyAuthenticationOptionsRequest = z.infer<
typeof ZCreatePasskeyAuthenticationOptionsRequestSchema
>;
export type TCreatePasskeyAuthenticationOptionsResponse = z.infer<
typeof ZCreatePasskeyAuthenticationOptionsResponseSchema
>;

View File

@ -0,0 +1,16 @@
import { createPasskeyRegistrationOptions } from '@documenso/lib/server-only/auth/create-passkey-registration-options';
import { authenticatedProcedure } from '../trpc';
import {
ZCreatePasskeyRegistrationOptionsRequestSchema,
ZCreatePasskeyRegistrationOptionsResponseSchema,
} from './create-passkey-registration-options.types';
export const createPasskeyRegistrationOptionsRoute = authenticatedProcedure
.input(ZCreatePasskeyRegistrationOptionsRequestSchema)
.output(ZCreatePasskeyRegistrationOptionsResponseSchema)
.mutation(async ({ ctx }) => {
return await createPasskeyRegistrationOptions({
userId: ctx.user.id,
});
});

View File

@ -0,0 +1,12 @@
import { z } from 'zod';
export const ZCreatePasskeyRegistrationOptionsRequestSchema = z.void();
export const ZCreatePasskeyRegistrationOptionsResponseSchema = z.any(); // PublicKeyCredentialCreationOptions type
export type TCreatePasskeyRegistrationOptionsRequest = z.infer<
typeof ZCreatePasskeyRegistrationOptionsRequestSchema
>;
export type TCreatePasskeyRegistrationOptionsResponse = z.infer<
typeof ZCreatePasskeyRegistrationOptionsResponseSchema
>;

View File

@ -0,0 +1,24 @@
import { createPasskeySigninOptions } from '@documenso/lib/server-only/auth/create-passkey-signin-options';
import { nanoid } from '@documenso/lib/universal/id';
import { procedure } from '../trpc';
import {
ZCreatePasskeySigninOptionsRequestSchema,
ZCreatePasskeySigninOptionsResponseSchema,
} from './create-passkey-signin-options.types';
export const createPasskeySigninOptionsRoute = procedure
.input(ZCreatePasskeySigninOptionsRequestSchema)
.output(ZCreatePasskeySigninOptionsResponseSchema)
.mutation(async () => {
const sessionIdToken = nanoid(16);
const [sessionId] = decodeURI(sessionIdToken).split('|');
const options = await createPasskeySigninOptions({ sessionId });
return {
options,
sessionId,
};
});

View File

@ -0,0 +1,15 @@
import { z } from 'zod';
export const ZCreatePasskeySigninOptionsRequestSchema = z.void();
export const ZCreatePasskeySigninOptionsResponseSchema = z.object({
options: z.any(), // PublicKeyCredentialRequestOptions type
sessionId: z.string(),
});
export type TCreatePasskeySigninOptionsRequest = z.infer<
typeof ZCreatePasskeySigninOptionsRequestSchema
>;
export type TCreatePasskeySigninOptionsResponse = z.infer<
typeof ZCreatePasskeySigninOptionsResponseSchema
>;

View File

@ -0,0 +1,21 @@
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';
import { authenticatedProcedure } from '../trpc';
import { ZCreatePasskeyRequestSchema, ZCreatePasskeyResponseSchema } from './create-passkey.types';
export const createPasskeyRoute = authenticatedProcedure
.input(ZCreatePasskeyRequestSchema)
.output(ZCreatePasskeyResponseSchema)
.mutation(async ({ ctx, input }) => {
// 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: ctx.metadata.requestMetadata,
});
});

View File

@ -0,0 +1,13 @@
import { z } from 'zod';
import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn';
export const ZCreatePasskeyRequestSchema = z.object({
passkeyName: z.string().trim().min(1),
verificationResponse: ZRegistrationResponseJSONSchema,
});
export const ZCreatePasskeyResponseSchema = z.void();
export type TCreatePasskeyRequest = z.infer<typeof ZCreatePasskeyRequestSchema>;
export type TCreatePasskeyResponse = z.infer<typeof ZCreatePasskeyResponseSchema>;

View File

@ -0,0 +1,23 @@
import { deletePasskey } from '@documenso/lib/server-only/auth/delete-passkey';
import { authenticatedProcedure } from '../trpc';
import { ZDeletePasskeyRequestSchema, ZDeletePasskeyResponseSchema } from './delete-passkey.types';
export const deletePasskeyRoute = authenticatedProcedure
.input(ZDeletePasskeyRequestSchema)
.output(ZDeletePasskeyResponseSchema)
.mutation(async ({ ctx, input }) => {
const { passkeyId } = input;
ctx.logger.info({
input: {
passkeyId,
},
});
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: ctx.metadata.requestMetadata,
});
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeletePasskeyRequestSchema = z.object({
passkeyId: z.string().trim().min(1),
});
export const ZDeletePasskeyResponseSchema = z.void();
export type TDeletePasskeyRequest = z.infer<typeof ZDeletePasskeyRequestSchema>;
export type TDeletePasskeyResponse = z.infer<typeof ZDeletePasskeyResponseSchema>;

View File

@ -0,0 +1,18 @@
import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
import { authenticatedProcedure } from '../trpc';
import { ZFindPasskeysRequestSchema, ZFindPasskeysResponseSchema } from './find-passkeys.types';
export const findPasskeysRoute = authenticatedProcedure
.input(ZFindPasskeysRequestSchema)
.output(ZFindPasskeysResponseSchema)
.query(async ({ input, ctx }) => {
const { page, perPage, orderBy } = input;
return await findPasskeys({
page,
perPage,
orderBy,
userId: ctx.user.id,
});
});

View File

@ -0,0 +1,33 @@
import { z } from 'zod';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import PasskeySchema from '@documenso/prisma/generated/zod/modelSchema/PasskeySchema';
export const ZFindPasskeysRequestSchema = ZFindSearchParamsSchema.extend({
orderBy: z
.object({
column: z.enum(['createdAt', 'updatedAt', 'name']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
});
export const ZFindPasskeysResponseSchema = ZFindResultResponse.extend({
data: z.array(
PasskeySchema.pick({
id: true,
userId: true,
name: true,
createdAt: true,
updatedAt: true,
lastUsedAt: true,
counter: true,
credentialDeviceType: true,
credentialBackedUp: true,
transports: true,
}),
),
});
export type TFindPasskeysRequest = z.infer<typeof ZFindPasskeysRequestSchema>;
export type TFindPasskeysResponse = z.infer<typeof ZFindPasskeysResponseSchema>;

View File

@ -1,113 +1,20 @@
import type { RegistrationResponseJSON } from '@simplewebauthn/types';
import { createPasskey } from '@documenso/lib/server-only/auth/create-passkey';
import { createPasskeyAuthenticationOptions } from '@documenso/lib/server-only/auth/create-passkey-authentication-options';
import { createPasskeyRegistrationOptions } from '@documenso/lib/server-only/auth/create-passkey-registration-options';
import { createPasskeySigninOptions } from '@documenso/lib/server-only/auth/create-passkey-signin-options';
import { deletePasskey } from '@documenso/lib/server-only/auth/delete-passkey';
import { findPasskeys } from '@documenso/lib/server-only/auth/find-passkeys';
import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
import { nanoid } from '@documenso/lib/universal/id';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZCreatePasskeyAuthenticationOptionsMutationSchema,
ZCreatePasskeyMutationSchema,
ZDeletePasskeyMutationSchema,
ZFindPasskeysQuerySchema,
ZUpdatePasskeyMutationSchema,
} from './schema';
import { router } from '../trpc';
import { createPasskeyRoute } from './create-passkey';
import { createPasskeyAuthenticationOptionsRoute } from './create-passkey-authentication-options';
import { createPasskeyRegistrationOptionsRoute } from './create-passkey-registration-options';
import { createPasskeySigninOptionsRoute } from './create-passkey-signin-options';
import { deletePasskeyRoute } from './delete-passkey';
import { findPasskeysRoute } from './find-passkeys';
import { updatePasskeyRoute } from './update-passkey';
export const authRouter = router({
createPasskey: authenticatedProcedure
.input(ZCreatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
// 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: ctx.metadata.requestMetadata,
});
}),
createPasskeyAuthenticationOptions: authenticatedProcedure
.input(ZCreatePasskeyAuthenticationOptionsMutationSchema)
.mutation(async ({ ctx, input }) => {
return await createPasskeyAuthenticationOptions({
userId: ctx.user.id,
preferredPasskeyId: input?.preferredPasskeyId,
});
}),
createPasskeyRegistrationOptions: authenticatedProcedure.mutation(async ({ ctx }) => {
return await createPasskeyRegistrationOptions({
userId: ctx.user.id,
});
passkey: router({
create: createPasskeyRoute,
createAuthenticationOptions: createPasskeyAuthenticationOptionsRoute,
createRegistrationOptions: createPasskeyRegistrationOptionsRoute,
createSigninOptions: createPasskeySigninOptionsRoute,
delete: deletePasskeyRoute,
find: findPasskeysRoute,
update: updatePasskeyRoute,
}),
createPasskeySigninOptions: procedure.mutation(async () => {
const sessionIdToken = nanoid(16);
const [sessionId] = decodeURI(sessionIdToken).split('|');
const options = await createPasskeySigninOptions({ sessionId });
return {
options,
sessionId,
};
}),
deletePasskey: authenticatedProcedure
.input(ZDeletePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
const { passkeyId } = input;
ctx.logger.info({
input: {
passkeyId,
},
});
await deletePasskey({
userId: ctx.user.id,
passkeyId,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
findPasskeys: authenticatedProcedure
.input(ZFindPasskeysQuerySchema)
.query(async ({ input, ctx }) => {
const { page, perPage, orderBy } = input;
return await findPasskeys({
page,
perPage,
orderBy,
userId: ctx.user.id,
});
}),
updatePasskey: authenticatedProcedure
.input(ZUpdatePasskeyMutationSchema)
.mutation(async ({ ctx, input }) => {
const { passkeyId, name } = input;
ctx.logger.info({
input: {
passkeyId,
},
});
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: ctx.metadata.requestMetadata,
});
}),
});

View File

@ -1,8 +1,5 @@
import { z } from 'zod';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import { ZRegistrationResponseJSONSchema } from '@documenso/lib/types/webauthn';
export const ZCurrentPasswordSchema = z
.string()
.min(6, { message: 'Must be at least 6 characters in length' })
@ -24,50 +21,3 @@ export const ZPasswordSchema = z
.refine((value) => value.length > 25 || /[`~<>?,./!@#$%^&*()\-_"'+=|{}[\];:\\]/.test(value), {
message: 'One special character is required',
});
export const ZSignUpMutationSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
password: ZPasswordSchema,
signature: z.string().nullish(),
url: z
.string()
.trim()
.toLowerCase()
.min(1)
.regex(/^[a-z0-9-]+$/, {
message: 'Username can only container alphanumeric characters and dashes.',
})
.optional(),
});
export const ZCreatePasskeyMutationSchema = z.object({
passkeyName: z.string().trim().min(1),
verificationResponse: ZRegistrationResponseJSONSchema,
});
export const ZCreatePasskeyAuthenticationOptionsMutationSchema = z
.object({
preferredPasskeyId: z.string().optional(),
})
.optional();
export const ZDeletePasskeyMutationSchema = z.object({
passkeyId: z.string().trim().min(1),
});
export const ZUpdatePasskeyMutationSchema = z.object({
passkeyId: z.string().trim().min(1),
name: z.string().trim().min(1),
});
export const ZFindPasskeysQuerySchema = ZFindSearchParamsSchema.extend({
orderBy: z
.object({
column: z.enum(['createdAt', 'updatedAt', 'name']),
direction: z.enum(['asc', 'desc']),
})
.optional(),
});
export type TSignUpMutationSchema = z.infer<typeof ZSignUpMutationSchema>;

View File

@ -0,0 +1,24 @@
import { updatePasskey } from '@documenso/lib/server-only/auth/update-passkey';
import { authenticatedProcedure } from '../trpc';
import { ZUpdatePasskeyRequestSchema, ZUpdatePasskeyResponseSchema } from './update-passkey.types';
export const updatePasskeyRoute = authenticatedProcedure
.input(ZUpdatePasskeyRequestSchema)
.output(ZUpdatePasskeyResponseSchema)
.mutation(async ({ ctx, input }) => {
const { passkeyId, name } = input;
ctx.logger.info({
input: {
passkeyId,
},
});
await updatePasskey({
userId: ctx.user.id,
passkeyId,
name,
requestMetadata: ctx.metadata.requestMetadata,
});
});

View File

@ -0,0 +1,11 @@
import { z } from 'zod';
export const ZUpdatePasskeyRequestSchema = z.object({
passkeyId: z.string().trim().min(1),
name: z.string().trim().min(1),
});
export const ZUpdatePasskeyResponseSchema = z.void();
export type TUpdatePasskeyRequest = z.infer<typeof ZUpdatePasskeyRequestSchema>;
export type TUpdatePasskeyResponse = z.infer<typeof ZUpdatePasskeyResponseSchema>;

View File

@ -0,0 +1,92 @@
import { DocumentDataType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { isValidExpirySettings } from '@documenso/lib/utils/expiry';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateDocumentTemporaryRequestSchema,
ZCreateDocumentTemporaryResponseSchema,
createDocumentTemporaryMeta,
} from './create-document-temporary.types';
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
* @deprecated
*/
export const createDocumentTemporaryRoute = authenticatedProcedure
.meta(createDocumentTemporaryMeta)
.input(ZCreateDocumentTemporaryRequestSchema)
.output(ZCreateDocumentTemporaryResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const {
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
meta,
folderId,
expiryAmount,
expiryUnit,
} = input;
// Validate expiry settings
if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid expiry settings. Please check your expiry configuration.',
});
}
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const documentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const createdDocument = await createDocumentV2({
userId: ctx.user.id,
teamId,
documentDataId: documentData.id,
normalizePdf: false, // Not normalizing because of presigned URL.
data: {
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
folderId,
expiryAmount,
expiryUnit,
},
meta,
requestMetadata: ctx.metadata,
});
return {
document: createdDocument,
uploadUrl: url,
};
});

View File

@ -0,0 +1,124 @@
import { DocumentSigningOrder } from '@prisma/client';
import { z } from 'zod';
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 { 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 { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentExpiryAmountSchema,
ZDocumentExpiryUnitSchema,
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from './schema';
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
*/
export const createDocumentTemporaryMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/create/beta',
summary: 'Create document',
description:
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
tags: ['Document'],
},
};
export const ZCreateDocumentTemporaryRequestSchema = z.object({
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(),
folderId: z
.string()
.describe(
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
)
.optional(),
expiryAmount: ZDocumentExpiryAmountSchema.optional(),
expiryUnit: ZDocumentExpiryUnitSchema.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.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(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZCreateDocumentTemporaryResponseSchema = 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 type TCreateDocumentTemporaryRequest = z.infer<typeof ZCreateDocumentTemporaryRequestSchema>;
export type TCreateDocumentTemporaryResponse = z.infer<
typeof ZCreateDocumentTemporaryResponseSchema
>;

View File

@ -0,0 +1,57 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { isValidExpirySettings } from '@documenso/lib/utils/expiry';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateDocumentRequestSchema,
ZCreateDocumentResponseSchema,
} from './create-document.types';
export const createDocumentRoute = authenticatedProcedure
.input(ZCreateDocumentRequestSchema)
.output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId, expiryAmount, expiryUnit } = input;
// Validate expiry settings
if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid expiry settings. Please check your expiry configuration.',
});
}
ctx.logger.info({
input: {
folderId,
},
});
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
const document = await createDocument({
userId: user.id,
teamId,
title,
documentDataId,
normalizePdf: true,
userTimezone: timezone,
requestMetadata: ctx.metadata,
folderId,
expiryAmount,
expiryUnit,
});
return {
id: document.id,
};
});

View File

@ -0,0 +1,34 @@
import { z } from 'zod';
import {
ZDocumentExpiryAmountSchema,
ZDocumentExpiryUnitSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentTitleSchema,
} from './schema';
// Currently not in use until we allow passthrough documents on create.
// export const createDocumentMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/document/create',
// summary: 'Create document',
// tags: ['Document'],
// },
// };
export const ZCreateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string().min(1),
timezone: ZDocumentMetaTimezoneSchema.optional(),
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
expiryAmount: ZDocumentExpiryAmountSchema.optional(),
expiryUnit: ZDocumentExpiryUnitSchema.optional(),
});
export const ZCreateDocumentResponseSchema = z.object({
id: z.number(),
});
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;

View File

@ -0,0 +1,35 @@
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteDocumentRequestSchema,
ZDeleteDocumentResponseSchema,
deleteDocumentMeta,
} from './delete-document.types';
import { ZGenericSuccessResponse } from './schema';
export const deleteDocumentRoute = authenticatedProcedure
.meta(deleteDocumentMeta)
.input(ZDeleteDocumentRequestSchema)
.output(ZDeleteDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const userId = ctx.user.id;
await deleteDocument({
id: documentId,
userId,
teamId,
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
});

View File

@ -0,0 +1,22 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
import { ZSuccessResponseSchema } from './schema';
export const deleteDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/delete',
summary: 'Delete document',
tags: ['Document'],
},
};
export const ZDeleteDocumentRequestSchema = z.object({
documentId: z.number(),
});
export const ZDeleteDocumentResponseSchema = ZSuccessResponseSchema;
export type TDeleteDocumentRequest = z.infer<typeof ZDeleteDocumentRequestSchema>;
export type TDeleteDocumentResponse = z.infer<typeof ZDeleteDocumentResponseSchema>;

View File

@ -0,0 +1,50 @@
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { authenticatedProcedure } from '../trpc';
import {
ZDistributeDocumentRequestSchema,
ZDistributeDocumentResponseSchema,
distributeDocumentMeta,
} from './distribute-document.types';
export const distributeDocumentRoute = authenticatedProcedure
.meta(distributeDocumentMeta)
.input(ZDistributeDocumentRequestSchema)
.output(ZDistributeDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, meta = {} } = input;
ctx.logger.info({
input: {
documentId,
},
});
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings,
language: meta.language,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
requestMetadata: ctx.metadata,
});
}
return await sendDocument({
userId: ctx.user.id,
documentId,
teamId,
requestMetadata: ctx.metadata,
});
});

View File

@ -0,0 +1,48 @@
import { z } from 'zod';
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
} from './schema';
export const distributeDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/distribute',
summary: 'Distribute document',
description: 'Send the document out to recipients based on your distribution method',
tags: ['Document'],
},
};
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(),
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
export type TDistributeDocumentRequest = z.infer<typeof ZDistributeDocumentRequestSchema>;
export type TDistributeDocumentResponse = z.infer<typeof ZDistributeDocumentResponseSchema>;

View File

@ -0,0 +1,47 @@
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { authenticatedProcedure } from '../trpc';
import {
ZDownloadDocumentAuditLogsRequestSchema,
ZDownloadDocumentAuditLogsResponseSchema,
} from './download-document-audit-logs.types';
export const downloadDocumentAuditLogsRoute = authenticatedProcedure
.input(ZDownloadDocumentAuditLogsRequestSchema)
.output(ZDownloadDocumentAuditLogsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const document = await getDocumentById({
documentId,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have access to this document.',
});
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
};
});

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
export const ZDownloadDocumentAuditLogsRequestSchema = z.object({
documentId: z.number(),
});
export const ZDownloadDocumentAuditLogsResponseSchema = z.object({
url: z.string(),
});
export type TDownloadDocumentAuditLogsRequest = z.infer<
typeof ZDownloadDocumentAuditLogsRequestSchema
>;
export type TDownloadDocumentAuditLogsResponse = z.infer<
typeof ZDownloadDocumentAuditLogsResponseSchema
>;

View File

@ -0,0 +1,46 @@
import { DateTime } from 'luxon';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
import {
ZDownloadDocumentCertificateRequestSchema,
ZDownloadDocumentCertificateResponseSchema,
} from './download-document-certificate.types';
export const downloadDocumentCertificateRoute = authenticatedProcedure
.input(ZDownloadDocumentCertificateRequestSchema)
.output(ZDownloadDocumentCertificateResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const document = await getDocumentById({
documentId,
userId: ctx.user.id,
teamId,
});
if (!isDocumentCompleted(document.status)) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
};
});

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
export const ZDownloadDocumentCertificateRequestSchema = z.object({
documentId: z.number(),
});
export const ZDownloadDocumentCertificateResponseSchema = z.object({
url: z.string(),
});
export type TDownloadDocumentCertificateRequest = z.infer<
typeof ZDownloadDocumentCertificateRequestSchema
>;
export type TDownloadDocumentCertificateResponse = z.infer<
typeof ZDownloadDocumentCertificateResponseSchema
>;

View File

@ -6,18 +6,14 @@ import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { authenticatedProcedure } from '../trpc';
import { ZDownloadDocumentRequestSchema, ZDownloadDocumentResponseSchema } from './schema';
import {
ZDownloadDocumentRequestSchema,
ZDownloadDocumentResponseSchema,
downloadDocumentMeta,
} from './download-document.types';
export const downloadDocumentRoute = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/{documentId}/download-beta',
summary: 'Download document (beta)',
description: 'Get a pre-signed download URL for the original or signed version of a document',
tags: ['Document'],
},
})
.meta(downloadDocumentMeta)
.input(ZDownloadDocumentRequestSchema)
.output(ZDownloadDocumentResponseSchema)
.query(async ({ input, ctx }) => {

View File

@ -0,0 +1,32 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const downloadDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/document/{documentId}/download-beta',
summary: 'Download document (beta)',
description: 'Get a pre-signed download URL for the original or signed version of a document',
tags: ['Document'],
},
};
export const ZDownloadDocumentRequestSchema = z.object({
documentId: z.number().describe('The ID of the document to download.'),
version: z
.enum(['original', 'signed'])
.describe(
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
)
.default('signed'),
});
export const ZDownloadDocumentResponseSchema = z.object({
downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
filename: z.string().describe('The filename of the PDF file'),
contentType: z.string().describe('MIME type of the file'),
});
export type TDownloadDocumentRequest = z.infer<typeof ZDownloadDocumentRequestSchema>;
export type TDownloadDocumentResponse = z.infer<typeof ZDownloadDocumentResponseSchema>;

View File

@ -0,0 +1,29 @@
import { duplicateDocument } from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { authenticatedProcedure } from '../trpc';
import {
ZDuplicateDocumentRequestSchema,
ZDuplicateDocumentResponseSchema,
duplicateDocumentMeta,
} from './duplicate-document.types';
export const duplicateDocumentRoute = authenticatedProcedure
.meta(duplicateDocumentMeta)
.input(ZDuplicateDocumentRequestSchema)
.output(ZDuplicateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await duplicateDocument({
userId: user.id,
teamId,
documentId,
});
});

View File

@ -0,0 +1,23 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
export const duplicateDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/duplicate',
summary: 'Duplicate document',
tags: ['Document'],
},
};
export const ZDuplicateDocumentRequestSchema = z.object({
documentId: z.number(),
});
export const ZDuplicateDocumentResponseSchema = z.object({
documentId: z.number(),
});
export type TDuplicateDocumentRequest = z.infer<typeof ZDuplicateDocumentRequestSchema>;
export type TDuplicateDocumentResponse = z.infer<typeof ZDuplicateDocumentResponseSchema>;

View File

@ -0,0 +1,41 @@
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { authenticatedProcedure } from '../trpc';
import {
ZFindDocumentAuditLogsRequestSchema,
ZFindDocumentAuditLogsResponseSchema,
} from './find-document-audit-logs.types';
export const findDocumentAuditLogsRoute = authenticatedProcedure
.input(ZFindDocumentAuditLogsRequestSchema)
.output(ZFindDocumentAuditLogsResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const {
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderByColumn,
orderByDirection,
} = input;
ctx.logger.info({
input: {
documentId,
},
});
return await findDocumentAuditLogs({
userId: ctx.user.id,
teamId,
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
});

View File

@ -0,0 +1,20 @@
import { z } from 'zod';
import { ZDocumentAuditLogSchema } from '@documenso/lib/types/document-audit-logs';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
export const ZFindDocumentAuditLogsRequestSchema = ZFindSearchParamsSchema.extend({
documentId: z.number().min(1),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
});
export const ZFindDocumentAuditLogsResponseSchema = ZFindResultResponse.extend({
data: ZDocumentAuditLogSchema.array(),
nextCursor: z.string().optional(),
});
export type TFindDocumentAuditLogsRequest = z.infer<typeof ZFindDocumentAuditLogsRequestSchema>;
export type TFindDocumentAuditLogsResponse = z.infer<typeof ZFindDocumentAuditLogsResponseSchema>;

View File

@ -0,0 +1,74 @@
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { authenticatedProcedure } from '../trpc';
import {
ZFindDocumentsInternalRequestSchema,
ZFindDocumentsInternalResponseSchema,
} from './find-documents-internal.types';
export const findDocumentsInternalRoute = authenticatedProcedure
.input(ZFindDocumentsInternalRequestSchema)
.output(ZFindDocumentsInternalResponseSchema)
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const {
query,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
period,
senderIds,
folderId,
} = input;
const getStatOptions: GetStatsInput = {
user,
period,
search: query,
folderId,
};
if (teamId) {
const team = await getTeamById({ userId: user.id, teamId });
getStatOptions.team = {
teamId: team.id,
teamEmail: team.teamEmail?.email,
senderIds,
currentTeamMemberRole: team.currentTeamRole,
currentUserEmail: user.email,
userId: user.id,
};
}
const [stats, documents] = await Promise.all([
getStats(getStatOptions),
findDocuments({
userId: user.id,
teamId,
query,
templateId,
page,
perPage,
source,
status,
period,
senderIds,
folderId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
}),
]);
return {
...documents,
stats,
};
});

View File

@ -0,0 +1,29 @@
import { z } from 'zod';
import { ZDocumentManySchema } from '@documenso/lib/types/document';
import { ZFindResultResponse } from '@documenso/lib/types/search-params';
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { ZFindDocumentsRequestSchema } from './find-documents.types';
export const ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.extend({
period: z.enum(['7d', '14d', '30d']).optional(),
senderIds: z.array(z.number()).optional(),
status: z.nativeEnum(ExtendedDocumentStatus).optional(),
folderId: z.string().optional(),
});
export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
data: ZDocumentManySchema.array(),
stats: z.object({
[ExtendedDocumentStatus.DRAFT]: z.number(),
[ExtendedDocumentStatus.PENDING]: z.number(),
[ExtendedDocumentStatus.COMPLETED]: z.number(),
[ExtendedDocumentStatus.REJECTED]: z.number(),
[ExtendedDocumentStatus.INBOX]: z.number(),
[ExtendedDocumentStatus.ALL]: z.number(),
}),
});
export type TFindDocumentsInternalRequest = z.infer<typeof ZFindDocumentsInternalRequestSchema>;
export type TFindDocumentsInternalResponse = z.infer<typeof ZFindDocumentsInternalResponseSchema>;

View File

@ -0,0 +1,43 @@
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { authenticatedProcedure } from '../trpc';
import {
ZFindDocumentsMeta,
ZFindDocumentsRequestSchema,
ZFindDocumentsResponseSchema,
} from './find-documents.types';
export const findDocumentsRoute = authenticatedProcedure
.meta(ZFindDocumentsMeta)
.input(ZFindDocumentsRequestSchema)
.output(ZFindDocumentsResponseSchema)
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const {
query,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
folderId,
} = input;
const documents = await findDocuments({
userId: user.id,
teamId,
templateId,
query,
source,
status,
page,
perPage,
folderId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
return documents;
});

View File

@ -0,0 +1,42 @@
import { DocumentSource, DocumentStatus } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentManySchema } from '@documenso/lib/types/document';
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
import type { TrpcRouteMeta } from '../trpc';
export const ZFindDocumentsMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/document',
summary: 'Find documents',
description: 'Find documents based on a search criteria',
tags: ['Document'],
},
};
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(),
folderId: z.string().describe('Filter documents by folder ID').optional(),
orderByColumn: z.enum(['createdAt']).optional(),
orderByDirection: z.enum(['asc', 'desc']).describe('').default('desc'),
});
export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({
data: ZDocumentManySchema.array(),
});
export type TFindDocumentsRequest = z.infer<typeof ZFindDocumentsRequestSchema>;
export type TFindDocumentsResponse = z.infer<typeof ZFindDocumentsResponseSchema>;

View File

@ -0,0 +1,43 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZGetDocumentByTokenRequestSchema,
ZGetDocumentByTokenResponseSchema,
} from './get-document-by-token.types';
export const getDocumentByTokenRoute = authenticatedProcedure
.input(ZGetDocumentByTokenRequestSchema)
.output(ZGetDocumentByTokenResponseSchema)
.query(async ({ input, ctx }) => {
const { token } = input;
const document = await prisma.document.findFirst({
where: {
recipients: {
some: {
token,
email: ctx.user.email,
},
},
},
include: {
documentData: true,
},
});
if (!document) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
ctx.logger.info({
documentId: document.id,
});
return {
documentData: document.documentData,
};
});

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
export const ZGetDocumentByTokenRequestSchema = z.object({
token: z.string().min(1),
});
export const ZGetDocumentByTokenResponseSchema = z.object({
documentData: DocumentDataSchema,
});
export type TGetDocumentByTokenRequest = z.infer<typeof ZGetDocumentByTokenRequestSchema>;
export type TGetDocumentByTokenResponse = z.infer<typeof ZGetDocumentByTokenResponseSchema>;

View File

@ -0,0 +1,29 @@
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { authenticatedProcedure } from '../trpc';
import {
ZGetDocumentRequestSchema,
ZGetDocumentResponseSchema,
getDocumentMeta,
} from './get-document.types';
export const getDocumentRoute = authenticatedProcedure
.meta(getDocumentMeta)
.input(ZGetDocumentRequestSchema)
.output(ZGetDocumentResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await getDocumentWithDetailsById({
userId: user.id,
teamId,
documentId,
});
});

View File

@ -0,0 +1,24 @@
import { z } from 'zod';
import { ZDocumentSchema } from '@documenso/lib/types/document';
import type { TrpcRouteMeta } from '../trpc';
export const getDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/document/{documentId}',
summary: 'Get document',
description: 'Returns a document given an ID',
tags: ['Document'],
},
};
export const ZGetDocumentRequestSchema = z.object({
documentId: z.number(),
});
export const ZGetDocumentResponseSchema = ZDocumentSchema;
export type TGetDocumentRequest = z.infer<typeof ZGetDocumentRequestSchema>;
export type TGetDocumentResponse = z.infer<typeof ZGetDocumentResponseSchema>;

View File

@ -0,0 +1,35 @@
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { authenticatedProcedure } from '../trpc';
import {
ZRedistributeDocumentRequestSchema,
ZRedistributeDocumentResponseSchema,
redistributeDocumentMeta,
} from './redistribute-document.types';
import { ZGenericSuccessResponse } from './schema';
export const redistributeDocumentRoute = authenticatedProcedure
.meta(redistributeDocumentMeta)
.input(ZRedistributeDocumentRequestSchema)
.output(ZRedistributeDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
ctx.logger.info({
input: {
documentId,
recipients,
},
});
await resendDocument({
userId: ctx.user.id,
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
});

View File

@ -0,0 +1,28 @@
import { z } from 'zod';
import type { TrpcRouteMeta } from '../trpc';
import { ZSuccessResponseSchema } from './schema';
export const redistributeDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/redistribute',
summary: 'Redistribute document',
description:
'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
tags: ['Document'],
},
};
export const ZRedistributeDocumentRequestSchema = z.object({
documentId: z.number(),
recipients: z
.array(z.number())
.min(1)
.describe('The IDs of the recipients to redistribute the document to.'),
});
export const ZRedistributeDocumentResponseSchema = ZSuccessResponseSchema;
export type TRedistributeDocumentRequest = z.infer<typeof ZRedistributeDocumentRequestSchema>;
export type TRedistributeDocumentResponse = z.infer<typeof ZRedistributeDocumentResponseSchema>;

View File

@ -1,709 +1,49 @@
import { DocumentDataType } from '@prisma/client';
import { DateTime } from 'luxon';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { duplicateDocument } from '@documenso/lib/server-only/document/duplicate-document-by-id';
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats';
import { getStats } from '@documenso/lib/server-only/document/get-stats';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { isValidExpirySettings } from '@documenso/lib/utils/expiry';
import { authenticatedProcedure, procedure, router } from '../trpc';
import { router } from '../trpc';
import { createDocumentRoute } from './create-document';
import { createDocumentTemporaryRoute } from './create-document-temporary';
import { deleteDocumentRoute } from './delete-document';
import { distributeDocumentRoute } from './distribute-document';
import { downloadDocumentRoute } from './download-document';
import { downloadDocumentAuditLogsRoute } from './download-document-audit-logs';
import { downloadDocumentCertificateRoute } from './download-document-certificate';
import { duplicateDocumentRoute } from './duplicate-document';
import { findDocumentAuditLogsRoute } from './find-document-audit-logs';
import { findDocumentsRoute } from './find-documents';
import { findDocumentsInternalRoute } from './find-documents-internal';
import { findInboxRoute } from './find-inbox';
import { getDocumentRoute } from './get-document';
import { getDocumentByTokenRoute } from './get-document-by-token';
import { getInboxCountRoute } from './get-inbox-count';
import {
ZCreateDocumentRequestSchema,
ZCreateDocumentV2RequestSchema,
ZCreateDocumentV2ResponseSchema,
ZDeleteDocumentMutationSchema,
ZDistributeDocumentRequestSchema,
ZDistributeDocumentResponseSchema,
ZDownloadAuditLogsMutationSchema,
ZDownloadCertificateMutationSchema,
ZDuplicateDocumentRequestSchema,
ZDuplicateDocumentResponseSchema,
ZFindDocumentAuditLogsQuerySchema,
ZFindDocumentsInternalRequestSchema,
ZFindDocumentsInternalResponseSchema,
ZFindDocumentsRequestSchema,
ZFindDocumentsResponseSchema,
ZGenericSuccessResponse,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZGetDocumentWithDetailsByIdRequestSchema,
ZGetDocumentWithDetailsByIdResponseSchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSetSigningOrderForDocumentMutationSchema,
ZSuccessResponseSchema,
} from './schema';
import { redistributeDocumentRoute } from './redistribute-document';
import { searchDocumentRoute } from './search-document';
import { updateDocumentRoute } from './update-document';
export const documentRouter = router({
inbox: {
get: getDocumentRoute,
find: findDocumentsRoute,
create: createDocumentRoute,
update: updateDocumentRoute,
delete: deleteDocumentRoute,
duplicate: duplicateDocumentRoute,
downloadCertificate: downloadDocumentCertificateRoute,
distribute: distributeDocumentRoute,
redistribute: redistributeDocumentRoute,
search: searchDocumentRoute,
// Temporary v2 beta routes to be removed once V2 is fully released.
download: downloadDocumentRoute,
createDocumentTemporary: createDocumentTemporaryRoute,
// Internal document routes for custom frontend requests.
getDocumentByToken: getDocumentByTokenRoute,
findDocumentsInternal: findDocumentsInternalRoute,
auditLog: {
find: findDocumentAuditLogsRoute,
download: downloadDocumentAuditLogsRoute,
},
inbox: router({
find: findInboxRoute,
getCount: getInboxCountRoute,
},
updateDocument: updateDocumentRoute,
downloadDocument: downloadDocumentRoute,
/**
* @private
*/
getDocumentById: authenticatedProcedure
.input(ZGetDocumentByIdQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await getDocumentById({
userId: ctx.user.id,
teamId,
documentId,
});
}),
/**
* @private
*/
getDocumentByToken: procedure
.input(ZGetDocumentByTokenQuerySchema)
.query(async ({ input, ctx }) => {
const { token } = input;
return await getDocumentAndSenderByToken({
token,
userId: ctx.user?.id,
});
}),
/**
* @public
*/
findDocuments: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document',
summary: 'Find documents',
description: 'Find documents based on a search criteria',
tags: ['Document'],
},
})
.input(ZFindDocumentsRequestSchema)
.output(ZFindDocumentsResponseSchema)
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const {
query,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
folderId,
} = input;
const documents = await findDocuments({
userId: user.id,
teamId,
templateId,
query,
source,
status,
page,
perPage,
folderId,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
return documents;
}),
/**
* Internal endpoint for /documents page to additionally return getStats.
*
* @private
*/
findDocumentsInternal: authenticatedProcedure
.input(ZFindDocumentsInternalRequestSchema)
.output(ZFindDocumentsInternalResponseSchema)
.query(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const {
query,
templateId,
page,
perPage,
orderByDirection,
orderByColumn,
source,
status,
period,
senderIds,
folderId,
} = input;
const getStatOptions: GetStatsInput = {
user,
period,
search: query,
folderId,
};
if (teamId) {
const team = await getTeamById({ userId: user.id, teamId });
getStatOptions.team = {
teamId: team.id,
teamEmail: team.teamEmail?.email,
senderIds,
currentTeamMemberRole: team.currentTeamRole,
currentUserEmail: user.email,
userId: user.id,
};
}
const [stats, documents] = await Promise.all([
getStats(getStatOptions),
findDocuments({
userId: user.id,
teamId,
query,
templateId,
page,
perPage,
source,
status,
period,
senderIds,
folderId,
orderBy: orderByColumn
? { column: orderByColumn, direction: orderByDirection }
: undefined,
}),
]);
return {
...documents,
stats,
};
}),
/**
* @public
*
* Todo: Refactor to getDocumentById.
*/
getDocumentWithDetailsById: authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/document/{documentId}',
summary: 'Get document',
description: 'Returns a document given an ID',
tags: ['Document'],
},
})
.input(ZGetDocumentWithDetailsByIdRequestSchema)
.output(ZGetDocumentWithDetailsByIdResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId, folderId } = input;
ctx.logger.info({
input: {
documentId,
folderId,
},
});
return await getDocumentWithDetailsById({
userId: user.id,
teamId,
documentId,
folderId,
});
}),
/**
* Temporariy endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
* @deprecated
*/
createDocumentTemporary: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/create/beta',
summary: 'Create document',
description:
'You will need to upload the PDF to the provided URL returned. Note: Once V2 API is released, this will be removed since we will allow direct uploads, instead of using an upload URL.',
tags: ['Document'],
},
})
.input(ZCreateDocumentV2RequestSchema)
.output(ZCreateDocumentV2ResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const {
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
meta,
expiryAmount,
expiryUnit,
} = input;
if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid expiry settings. Please check your expiry configuration.',
});
}
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
const fileName = title.endsWith('.pdf') ? title : `${title}.pdf`;
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
const documentData = await createDocumentData({
data: key,
type: DocumentDataType.S3_PATH,
});
const createdDocument = await createDocumentV2({
userId: ctx.user.id,
teamId,
documentDataId: documentData.id,
normalizePdf: false, // Not normalizing because of presigned URL.
data: {
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
expiryAmount,
expiryUnit,
},
meta,
requestMetadata: ctx.metadata,
});
return {
document: createdDocument,
folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release.
uploadUrl: url,
};
}),
/**
* Wait until RR7 so we can passthrough documents.
*
* @private
*/
createDocument: authenticatedProcedure
// .meta({
// openapi: {
// method: 'POST',
// path: '/document/create',
// summary: 'Create document',
// tags: ['Document'],
// },
// })
.input(ZCreateDocumentRequestSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId, expiryAmount, expiryUnit } = input;
// Validate expiry settings
if ((expiryAmount || expiryUnit) && !isValidExpirySettings(expiryAmount, expiryUnit)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Invalid expiry settings. Please check your expiry configuration.',
});
}
ctx.logger.info({
input: {
folderId,
},
});
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
return await createDocument({
userId: user.id,
teamId,
title,
documentDataId,
normalizePdf: true,
userTimezone: timezone,
requestMetadata: ctx.metadata,
folderId,
expiryAmount,
expiryUnit,
});
}),
/**
* @public
*/
deleteDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/delete',
summary: 'Delete document',
tags: ['Document'],
},
})
.input(ZDeleteDocumentMutationSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const userId = ctx.user.id;
await deleteDocument({
id: documentId,
userId,
teamId,
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
}),
/**
* @private
*
* Todo: Remove and use `updateDocument` endpoint instead.
*/
setSigningOrderForDocument: authenticatedProcedure
.input(ZSetSigningOrderForDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, signingOrder } = input;
ctx.logger.info({
input: {
documentId,
signingOrder,
},
});
return await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
signingOrder,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*
* Todo: Refactor to distributeDocument.
* Todo: Rework before releasing API.
*/
sendDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/distribute',
summary: 'Distribute document',
description: 'Send the document out to recipients based on your distribution method',
tags: ['Document'],
},
})
.input(ZDistributeDocumentRequestSchema)
.output(ZDistributeDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, meta = {} } = input;
ctx.logger.info({
input: {
documentId,
},
});
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
dateFormat: meta.dateFormat,
timezone: meta.timezone,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
emailSettings: meta.emailSettings,
language: meta.language,
emailId: meta.emailId,
emailReplyTo: meta.emailReplyTo,
requestMetadata: ctx.metadata,
});
}
return await sendDocument({
userId: ctx.user.id,
documentId,
teamId,
requestMetadata: ctx.metadata,
});
}),
/**
* @public
*
* Todo: Refactor to redistributeDocument.
*/
resendDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/redistribute',
summary: 'Redistribute document',
description:
'Redistribute the document to the provided recipients who have not actioned the document. Will use the distribution method set in the document',
tags: ['Document'],
},
})
.input(ZResendDocumentMutationSchema)
.output(ZSuccessResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, recipients } = input;
ctx.logger.info({
input: {
documentId,
recipients,
},
});
await resendDocument({
userId: ctx.user.id,
teamId,
documentId,
recipients,
requestMetadata: ctx.metadata,
});
return ZGenericSuccessResponse;
}),
/**
* @public
*/
duplicateDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/duplicate',
summary: 'Duplicate document',
tags: ['Document'],
},
})
.input(ZDuplicateDocumentRequestSchema)
.output(ZDuplicateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
return await duplicateDocument({
userId: user.id,
teamId,
documentId,
});
}),
/**
* @private
*/
searchDocuments: authenticatedProcedure
.input(ZSearchDocumentsMutationSchema)
.query(async ({ input, ctx }) => {
const { query } = input;
const documents = await searchDocumentsWithKeyword({
query,
userId: ctx.user.id,
});
return documents;
}),
/**
* @private
*/
findDocumentAuditLogs: authenticatedProcedure
.input(ZFindDocumentAuditLogsQuerySchema)
.query(async ({ input, ctx }) => {
const { teamId } = ctx;
const {
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderByColumn,
orderByDirection,
} = input;
ctx.logger.info({
input: {
documentId,
},
});
return await findDocumentAuditLogs({
userId: ctx.user.id,
teamId,
page,
perPage,
documentId,
cursor,
filterForRecentActivity,
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
});
}),
/**
* @private
*/
downloadAuditLogs: authenticatedProcedure
.input(ZDownloadAuditLogsMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const document = await getDocumentById({
documentId,
userId: ctx.user.id,
teamId,
}).catch(() => null);
if (!document || (teamId && document.teamId !== teamId)) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have access to this document.',
});
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encrypted}`,
};
}),
/**
* @private
*/
downloadCertificate: authenticatedProcedure
.input(ZDownloadCertificateMutationSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId } = input;
ctx.logger.info({
input: {
documentId,
},
});
const document = await getDocumentById({
documentId,
userId: ctx.user.id,
teamId,
});
if (!isDocumentCompleted(document.status)) {
throw new AppError('DOCUMENT_NOT_COMPLETE');
}
const encrypted = encryptSecondaryData({
data: document.id.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
return {
url: `${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encrypted}`,
};
}),
}),
});

View File

@ -1,39 +1,9 @@
import {
DocumentDistributionMethod,
DocumentSigningOrder,
DocumentSource,
DocumentStatus,
DocumentVisibility,
FieldType,
} from '@prisma/client';
import { DocumentDistributionMethod, DocumentVisibility } from '@prisma/client';
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 { 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 { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
@ -126,256 +96,3 @@ export const ZDocumentExpiryAmountSchema = z
export const ZDocumentExpiryUnitSchema = z
.enum(['minutes', 'hours', 'days', 'weeks', 'months'])
.describe('The unit for expiry duration (e.g., "days" for "3 days").');
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(),
folderId: z.string().describe('Filter documents by folder ID').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 ZFindDocumentsInternalRequestSchema = ZFindDocumentsRequestSchema.extend({
period: z.enum(['7d', '14d', '30d']).optional(),
senderIds: z.array(z.number()).optional(),
status: z.nativeEnum(ExtendedDocumentStatus).optional(),
folderId: z.string().optional(),
});
export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({
data: ZDocumentManySchema.array(),
stats: z.object({
[ExtendedDocumentStatus.DRAFT]: z.number(),
[ExtendedDocumentStatus.PENDING]: z.number(),
[ExtendedDocumentStatus.COMPLETED]: z.number(),
[ExtendedDocumentStatus.REJECTED]: z.number(),
[ExtendedDocumentStatus.INBOX]: z.number(),
[ExtendedDocumentStatus.ALL]: z.number(),
}),
});
export type TFindDocumentsInternalResponse = z.infer<typeof ZFindDocumentsInternalResponseSchema>;
export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({
documentId: z.number().min(1),
cursor: z.string().optional(),
filterForRecentActivity: z.boolean().optional(),
orderByColumn: z.enum(['createdAt', 'type']).optional(),
orderByDirection: z.enum(['asc', 'desc']).default('desc'),
});
export const ZGetDocumentByIdQuerySchema = z.object({
documentId: z.number(),
});
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),
});
export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQuerySchema>;
export const ZGetDocumentWithDetailsByIdRequestSchema = z.object({
documentId: z.number(),
folderId: z.string().describe('Filter documents by folder ID').optional(),
});
export const ZGetDocumentWithDetailsByIdResponseSchema = ZDocumentSchema;
export const ZCreateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string().min(1),
timezone: ZDocumentMetaTimezoneSchema.optional(),
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
expiryAmount: ZDocumentExpiryAmountSchema.optional(),
expiryUnit: ZDocumentExpiryUnitSchema.optional(),
});
export const ZCreateDocumentV2RequestSchema = z.object({
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(),
expiryAmount: ZDocumentExpiryAmountSchema.optional(),
expiryUnit: ZDocumentExpiryUnitSchema.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.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(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export type TCreateDocumentV2Request = z.infer<typeof ZCreateDocumentV2RequestSchema>;
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 ZSetFieldsForDocumentMutationSchema = z.object({
documentId: z.number(),
fields: z.array(
z.object({
id: z.number().nullish(),
type: z.nativeEnum(FieldType),
signerEmail: z.string().min(1),
pageNumber: z.number().min(1),
pageX: z.number().min(0),
pageY: z.number().min(0),
pageWidth: z.number().min(0),
pageHeight: z.number().min(0),
}),
),
});
export type TSetFieldsForDocumentMutationSchema = z.infer<
typeof ZSetFieldsForDocumentMutationSchema
>;
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(),
emailId: z.string().nullish(),
emailReplyTo: z.string().email().nullish(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZDistributeDocumentResponseSchema = ZDocumentLiteSchema;
export const ZSetPasswordForDocumentMutationSchema = z.object({
documentId: z.number(),
password: z.string(),
});
export type TSetPasswordForDocumentMutationSchema = z.infer<
typeof ZSetPasswordForDocumentMutationSchema
>;
export const ZSetSigningOrderForDocumentMutationSchema = z.object({
documentId: z.number(),
signingOrder: z.nativeEnum(DocumentSigningOrder),
});
export type TSetSigningOrderForDocumentMutationSchema = z.infer<
typeof ZSetSigningOrderForDocumentMutationSchema
>;
export const ZResendDocumentMutationSchema = z.object({
documentId: z.number(),
recipients: z
.array(z.number())
.min(1)
.describe('The IDs of the recipients to redistribute the document to.'),
});
export const ZDeleteDocumentMutationSchema = z.object({
documentId: z.number(),
});
export type TDeleteDocumentMutationSchema = z.infer<typeof ZDeleteDocumentMutationSchema>;
export const ZSearchDocumentsMutationSchema = z.object({
query: z.string(),
});
export const ZDownloadAuditLogsMutationSchema = z.object({
documentId: z.number(),
});
export const ZDownloadCertificateMutationSchema = z.object({
documentId: z.number(),
});
export const ZDownloadDocumentRequestSchema = z.object({
documentId: z.number().describe('The ID of the document to download.'),
version: z
.enum(['original', 'signed'])
.describe(
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
)
.default('signed'),
});
export const ZDownloadDocumentResponseSchema = z.object({
downloadUrl: z.string().describe('Pre-signed URL for downloading the PDF file'),
filename: z.string().describe('The filename of the PDF file'),
contentType: z.string().describe('MIME type of the file'),
});
export type TDownloadDocumentRequest = z.infer<typeof ZDownloadDocumentRequestSchema>;
export type TDownloadDocumentResponse = z.infer<typeof ZDownloadDocumentResponseSchema>;

View File

@ -0,0 +1,21 @@
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import { authenticatedProcedure } from '../trpc';
import {
ZSearchDocumentRequestSchema,
ZSearchDocumentResponseSchema,
} from './search-document.types';
export const searchDocumentRoute = authenticatedProcedure
.input(ZSearchDocumentRequestSchema)
.output(ZSearchDocumentResponseSchema)
.query(async ({ input, ctx }) => {
const { query } = input;
const documents = await searchDocumentsWithKeyword({
query,
userId: ctx.user.id,
});
return documents;
});

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
export const ZSearchDocumentRequestSchema = z.object({
query: z.string(),
});
export const ZSearchDocumentResponseSchema = z
.object({
title: z.string(),
path: z.string(),
value: z.string(),
})
.array();
export type TSearchDocumentRequest = z.infer<typeof ZSearchDocumentRequestSchema>;
export type TSearchDocumentResponse = z.infer<typeof ZSearchDocumentResponseSchema>;

View File

@ -40,7 +40,13 @@ export const createOrganisationGroupRoute = authenticatedProcedure
groups: true,
members: {
include: {
user: true,
user: {
select: {
id: true,
email: true,
name: true,
},
},
},
},
},

View File

@ -1,5 +1,6 @@
import { ORGANISATION_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/organisations';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { stripe } from '@documenso/lib/server-only/stripe';
import { buildOrganisationWhereQuery } from '@documenso/lib/utils/organisations';
import { prisma } from '@documenso/prisma';
@ -38,7 +39,7 @@ export const updateOrganisationRoute = authenticatedProcedure
});
}
await prisma.organisation.update({
const updatedOrganisation = await prisma.organisation.update({
where: {
id: organisationId,
},
@ -47,4 +48,12 @@ export const updateOrganisationRoute = authenticatedProcedure
url: data.url,
},
});
if (updatedOrganisation.customerId) {
await stripe.customers.update(updatedOrganisation.customerId, {
metadata: {
organisationName: data.name,
},
});
}
});

View File

@ -1,15 +1,16 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { SetAvatarImageOptions } from '@documenso/lib/server-only/profile/set-avatar-image';
import { setAvatarImage } from '@documenso/lib/server-only/profile/set-avatar-image';
import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { submitSupportTicket } from '@documenso/lib/server-only/user/submit-support-ticket';
import { updateProfile } from '@documenso/lib/server-only/user/update-profile';
import { adminProcedure, authenticatedProcedure, router } from '../trpc';
import { authenticatedProcedure, router } from '../trpc';
import {
ZFindUserSecurityAuditLogsSchema,
ZRetrieveUserByIdQuerySchema,
ZSetProfileImageMutationSchema,
ZSubmitSupportTicketMutationSchema,
ZUpdateProfileMutationSchema,
} from './schema';
@ -23,24 +24,12 @@ export const profileRouter = router({
});
}),
getUser: adminProcedure.input(ZRetrieveUserByIdQuerySchema).query(async ({ input, ctx }) => {
const { id } = input;
ctx.logger.info({
input: {
id,
},
});
return await getUserById({ id });
}),
updateProfile: authenticatedProcedure
.input(ZUpdateProfileMutationSchema)
.mutation(async ({ input, ctx }) => {
const { name, signature } = input;
return await updateProfile({
await updateProfile({
userId: ctx.user.id,
name,
signature,
@ -49,7 +38,7 @@ export const profileRouter = router({
}),
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
return await deleteUser({
await deleteUser({
id: ctx.user.id,
});
}),
@ -91,4 +80,28 @@ export const profileRouter = router({
requestMetadata: ctx.metadata,
});
}),
submitSupportTicket: authenticatedProcedure
.input(ZSubmitSupportTicketMutationSchema)
.mutation(async ({ input, ctx }) => {
const { subject, message, organisationId, teamId } = input;
const userId = ctx.user.id;
const parsedTeamId = teamId ? Number(teamId) : null;
if (Number.isNaN(parsedTeamId)) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: 'Invalid team ID provided',
});
}
return await submitSupportTicket({
subject,
message,
userId,
organisationId,
teamId: parsedTeamId,
});
}),
});

View File

@ -7,12 +7,6 @@ export const ZFindUserSecurityAuditLogsSchema = z.object({
export type TFindUserSecurityAuditLogsSchema = z.infer<typeof ZFindUserSecurityAuditLogsSchema>;
export const ZRetrieveUserByIdQuerySchema = z.object({
id: z.number().min(1),
});
export type TRetrieveUserByIdQuerySchema = z.infer<typeof ZRetrieveUserByIdQuerySchema>;
export const ZUpdateProfileMutationSchema = z.object({
name: z.string().min(1),
signature: z.string(),
@ -27,3 +21,12 @@ export const ZSetProfileImageMutationSchema = z.object({
});
export type TSetProfileImageMutationSchema = z.infer<typeof ZSetProfileImageMutationSchema>;
export const ZSubmitSupportTicketMutationSchema = z.object({
organisationId: z.string(),
teamId: z.string().min(1).nullish(),
subject: z.string().min(3, 'Subject is required'),
message: z.string().min(10, 'Message must be at least 10 characters'),
});
export type TSupportTicketRequest = z.infer<typeof ZSubmitSupportTicketMutationSchema>;

View File

@ -339,8 +339,14 @@ export const templateRouter = router({
.output(ZCreateDocumentFromTemplateResponseSchema)
.mutation(async ({ ctx, input }) => {
const { teamId } = ctx;
const { templateId, recipients, distributeDocument, customDocumentDataId, prefillFields } =
input;
const {
templateId,
recipients,
distributeDocument,
customDocumentDataId,
prefillFields,
folderId,
} = input;
ctx.logger.info({
input: {
@ -361,6 +367,7 @@ export const templateRouter = router({
recipients,
customDocumentDataId,
requestMetadata: ctx.metadata,
folderId,
prefillFields,
});

View File

@ -117,6 +117,12 @@ export const ZCreateDocumentFromTemplateRequestSchema = z.object({
'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(),
folderId: z
.string()
.describe(
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
)
.optional(),
prefillFields: z
.array(ZFieldMetaPrefillFieldsSchema)
.describe(