mirror of
https://github.com/documenso/documenso.git
synced 2026-10-03 10:13:52 +10:00
Merge branch 'main' into refactor/trpc-error-formatter
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
|
||||
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
|
||||
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZCreateApiTokenRequestSchema, ZCreateApiTokenResponseSchema } from './create-api-token.types';
|
||||
@@ -15,10 +18,26 @@ export const createApiTokenRoute = authenticatedProcedure
|
||||
},
|
||||
});
|
||||
|
||||
return await createApiToken({
|
||||
const createdToken = await createApiToken({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
tokenName,
|
||||
expiresIn: expirationDate,
|
||||
});
|
||||
|
||||
fireAndForget(async () => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: { id: teamId },
|
||||
select: { organisationId: true },
|
||||
});
|
||||
|
||||
captureServerEvent({
|
||||
event: 'App: API Token Created',
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
organisationId: team?.organisationId,
|
||||
});
|
||||
});
|
||||
|
||||
return createdToken;
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export const ZGetApiTokensResponseSchema = z.array(
|
||||
name: true,
|
||||
createdAt: true,
|
||||
expires: true,
|
||||
lastUsedAt: true,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getUserAuthMethods } from '@documenso/lib/server-only/user/get-user-auth-methods';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZGetAuthMethodsResponseSchema } from './get-auth-methods.types';
|
||||
|
||||
/**
|
||||
* Get the sign in methods available to the current user.
|
||||
*/
|
||||
export const getAuthMethodsRoute = authenticatedProcedure
|
||||
.output(ZGetAuthMethodsResponseSchema)
|
||||
.query(async ({ ctx }) => {
|
||||
const authMethods = await getUserAuthMethods({
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
authMethods,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ZUserAuthMethodSchema } from '@documenso/lib/types/user-auth-method';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZGetAuthMethodsResponseSchema = z.object({
|
||||
authMethods: z.array(ZUserAuthMethodSchema),
|
||||
});
|
||||
|
||||
export type TGetAuthMethodsResponse = z.infer<typeof ZGetAuthMethodsResponseSchema>;
|
||||
@@ -5,9 +5,11 @@ import { createPasskeyRegistrationOptionsRoute } from './create-passkey-registra
|
||||
import { createPasskeySigninOptionsRoute } from './create-passkey-signin-options';
|
||||
import { deletePasskeyRoute } from './delete-passkey';
|
||||
import { findPasskeysRoute } from './find-passkeys';
|
||||
import { getAuthMethodsRoute } from './get-auth-methods';
|
||||
import { updatePasskeyRoute } from './update-passkey';
|
||||
|
||||
export const authRouter = router({
|
||||
getAuthMethods: getAuthMethodsRoute,
|
||||
passkey: router({
|
||||
create: createPasskeyRoute,
|
||||
createAuthenticationOptions: createPasskeyAuthenticationOptionsRoute,
|
||||
|
||||
@@ -5,13 +5,13 @@ import type { Envelope, Prisma } from '@prisma/client';
|
||||
import { DocumentStatus, EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { ZFindInboxRequestSchema, ZFindInboxResponseSchema } from './find-inbox.types';
|
||||
import { type TInboxStatus, ZFindInboxRequestSchema, ZFindInboxResponseSchema } from './find-inbox.types';
|
||||
|
||||
export const findInboxRoute = authenticatedProcedure
|
||||
.input(ZFindInboxRequestSchema)
|
||||
.output(ZFindInboxResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { page, perPage } = input;
|
||||
const { page, perPage, query, status } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
@@ -19,6 +19,8 @@ export const findInboxRoute = authenticatedProcedure
|
||||
userId,
|
||||
page,
|
||||
perPage,
|
||||
query,
|
||||
status,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -31,13 +33,22 @@ export type FindInboxOptions = {
|
||||
userId: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
/**
|
||||
* Case insensitive search against the document title.
|
||||
*/
|
||||
query?: string;
|
||||
|
||||
/**
|
||||
* Restrict results to a single status. When omitted, every non-draft status is returned.
|
||||
*/
|
||||
status?: TInboxStatus;
|
||||
orderBy?: {
|
||||
column: keyof Omit<Envelope, 'envelope'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
};
|
||||
|
||||
export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: FindInboxOptions) => {
|
||||
export const findInbox = async ({ userId, page = 1, perPage = 10, query = '', status, orderBy }: FindInboxOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
@@ -50,10 +61,11 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
const searchQuery = query.trim();
|
||||
|
||||
const whereClause: Prisma.EnvelopeWhereInput = {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
status: {
|
||||
status: status ?? {
|
||||
not: DocumentStatus.DRAFT,
|
||||
},
|
||||
deletedAt: null,
|
||||
@@ -67,6 +79,13 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin
|
||||
},
|
||||
};
|
||||
|
||||
if (searchQuery.length > 0) {
|
||||
whereClause.title = {
|
||||
contains: searchQuery,
|
||||
mode: 'insensitive',
|
||||
};
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: whereClause,
|
||||
|
||||
@@ -2,12 +2,33 @@
|
||||
|
||||
import { ZDocumentManySchema } from '@documenso/lib/types/document';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
import type { z } from 'zod';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZFindInboxRequestSchema = ZFindSearchParamsSchema;
|
||||
/**
|
||||
* The statuses that can be filtered by in the inbox.
|
||||
*
|
||||
* Every document status except DRAFT, since drafts have not been sent to
|
||||
* recipients yet and must never be visible in the inbox.
|
||||
*/
|
||||
export const INBOX_STATUSES = [
|
||||
DocumentStatus.PENDING,
|
||||
DocumentStatus.COMPLETED,
|
||||
DocumentStatus.REJECTED,
|
||||
DocumentStatus.CANCELLED,
|
||||
] as const;
|
||||
|
||||
export const ZInboxStatusSchema = z.enum(INBOX_STATUSES);
|
||||
|
||||
export type TInboxStatus = z.infer<typeof ZInboxStatusSchema>;
|
||||
|
||||
export const ZFindInboxRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
status: ZInboxStatusSchema.describe('Filter the inbox by document status.').optional(),
|
||||
});
|
||||
|
||||
export const ZFindInboxResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZDocumentManySchema.array(),
|
||||
});
|
||||
|
||||
export type TFindInboxRequest = z.infer<typeof ZFindInboxRequestSchema>;
|
||||
export type TFindInboxResponse = z.infer<typeof ZFindInboxResponseSchema>;
|
||||
|
||||
@@ -18,6 +18,5 @@ export const embeddingPresignRouter = router({
|
||||
updateEmbeddingEnvelope: updateEmbeddingEnvelopeRoute,
|
||||
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
||||
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
||||
// applyMultiSignSignature: applyMultiSignSignatureRoute,
|
||||
getMultiSignDocument: getMultiSignDocumentRoute,
|
||||
});
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { FieldType, ReadStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { procedure } from '../trpc';
|
||||
import {
|
||||
ZApplyMultiSignSignatureRequestSchema,
|
||||
ZApplyMultiSignSignatureResponseSchema,
|
||||
} from './apply-multi-sign-signature.types';
|
||||
|
||||
export const applyMultiSignSignatureRoute = procedure
|
||||
.input(ZApplyMultiSignSignatureRequestSchema)
|
||||
.output(ZApplyMultiSignSignatureResponseSchema)
|
||||
.mutation(async ({ input, ctx: { metadata } }) => {
|
||||
try {
|
||||
const { tokens, signature, isBase64 } = input;
|
||||
|
||||
// Get all documents and recipients for the tokens
|
||||
const envelopes = await Promise.all(
|
||||
tokens.map(async (token) => {
|
||||
const document = await getDocumentByToken({ token });
|
||||
const recipient = await getRecipientByToken({ token });
|
||||
|
||||
return { document, recipient };
|
||||
}),
|
||||
);
|
||||
|
||||
// Check if all documents have been viewed
|
||||
const hasUnviewedDocuments = envelopes.some((envelope) => envelope.recipient.readStatus !== ReadStatus.OPENED);
|
||||
|
||||
if (hasUnviewedDocuments) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'All documents must be viewed before signing',
|
||||
});
|
||||
}
|
||||
|
||||
// If we require action auth we should abort here for now
|
||||
for (const envelope of envelopes) {
|
||||
const derivedRecipientActionAuth = extractDocumentAuthMethods({
|
||||
documentAuth: envelope.document.authOptions,
|
||||
recipientAuth: envelope.recipient.authOptions,
|
||||
});
|
||||
|
||||
if (
|
||||
derivedRecipientActionAuth.recipientAccessAuthRequired ||
|
||||
derivedRecipientActionAuth.recipientActionAuthRequired
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Documents that require additional authentication cannot be multi signed at the moment',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sign all signature fields for each document
|
||||
await Promise.all(
|
||||
envelopes.map(async (envelope) => {
|
||||
if (envelope.recipient.signingStatus === SigningStatus.REJECTED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signatureFields = await prisma.field.findMany({
|
||||
where: {
|
||||
envelopeId: envelope.document.id,
|
||||
recipientId: envelope.recipient.id,
|
||||
type: FieldType.SIGNATURE,
|
||||
inserted: false,
|
||||
},
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
signatureFields.map(async (field) =>
|
||||
signFieldWithToken({
|
||||
token: envelope.recipient.token,
|
||||
fieldId: field.id,
|
||||
value: signature,
|
||||
isBase64,
|
||||
requestMetadata: metadata.requestMetadata,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
|
||||
message: 'Failed to apply multi-sign signature',
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ZApplyMultiSignSignatureRequestSchema = z.object({
|
||||
tokens: z.array(z.string()).min(1, { message: 'At least one token is required' }),
|
||||
signature: z.string().min(1, { message: 'Signature is required' }),
|
||||
isBase64: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export const ZApplyMultiSignSignatureResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
export type TApplyMultiSignSignatureRequestSchema = z.infer<typeof ZApplyMultiSignSignatureRequestSchema>;
|
||||
export type TApplyMultiSignSignatureResponseSchema = z.infer<typeof ZApplyMultiSignSignatureResponseSchema>;
|
||||
@@ -8,13 +8,14 @@ export const cancelEnvelopeMeta: TrpcRouteMeta = {
|
||||
method: 'POST',
|
||||
path: '/envelope/cancel',
|
||||
summary: 'Cancel envelope',
|
||||
description: 'Cancel a pending envelope',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZCancelEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
reason: z.string().optional(),
|
||||
envelopeId: z.string().describe('The ID of the envelope to cancel.'),
|
||||
reason: z.string().describe('The reason for cancelling the envelope.').optional(),
|
||||
});
|
||||
|
||||
export const ZCancelEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import type { Logger } from 'pino';
|
||||
import { match, P } from 'ts-pattern';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
@@ -148,19 +149,11 @@ export const createEnvelopeRouteCaller = async ({
|
||||
accessAuth: recipient.accessAuth,
|
||||
actionAuth: recipient.actionAuth,
|
||||
fields: recipient.fields?.map((field) => {
|
||||
let documentDataId: string | undefined;
|
||||
|
||||
if (typeof field.identifier === 'string') {
|
||||
documentDataId = envelopeItems.find((item) => item.title === field.identifier)?.documentDataId;
|
||||
}
|
||||
|
||||
if (typeof field.identifier === 'number') {
|
||||
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
|
||||
}
|
||||
|
||||
if (field.identifier === undefined) {
|
||||
documentDataId = envelopeItems.at(0)?.documentDataId;
|
||||
}
|
||||
const documentDataId = match(field.identifier)
|
||||
.with(P.string, (title) => envelopeItems.find((item) => item.title === title)?.documentDataId)
|
||||
.with(P.number, (index) => envelopeItems.at(index)?.documentDataId)
|
||||
.with(undefined, () => envelopeItems.at(0)?.documentDataId)
|
||||
.exhaustive();
|
||||
|
||||
if (!documentDataId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
|
||||
@@ -8,12 +8,13 @@ export const deleteEnvelopeMeta: TrpcRouteMeta = {
|
||||
method: 'POST',
|
||||
path: '/envelope/delete',
|
||||
summary: 'Delete envelope',
|
||||
description: 'Delete an envelope',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZDeleteEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeId: z.string().describe('The ID of the envelope to delete.'),
|
||||
});
|
||||
|
||||
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
|
||||
|
||||
@@ -29,7 +29,17 @@ export const updateEnvelopeFieldsRoute = authenticatedProcedure
|
||||
id: envelopeId,
|
||||
},
|
||||
type: null,
|
||||
fields,
|
||||
fields: fields.map((field) => ({
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
pageNumber: field.page,
|
||||
pageX: field.positionX,
|
||||
pageY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
fieldMeta: field.fieldMeta,
|
||||
envelopeItemId: field.envelopeItemId,
|
||||
})),
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ export const signEnvelopeFieldRoute = procedure
|
||||
const field = await prisma.field.findFirst({
|
||||
where: {
|
||||
id: fieldId,
|
||||
recipient: {
|
||||
...(recipient.role === RecipientRole.ASSISTANT
|
||||
recipient:
|
||||
recipient.role === RecipientRole.ASSISTANT
|
||||
? {
|
||||
signingStatus: {
|
||||
not: SigningStatus.SIGNED,
|
||||
@@ -52,8 +52,7 @@ export const signEnvelopeFieldRoute = procedure
|
||||
}
|
||||
: {
|
||||
id: recipient.id,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
envelope: {
|
||||
|
||||
@@ -12,24 +12,32 @@ export const updateEnvelopeMeta: TrpcRouteMeta = {
|
||||
method: 'POST',
|
||||
path: '/envelope/update',
|
||||
summary: 'Update envelope',
|
||||
description: 'Update envelope properties and settings',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZUpdateEnvelopeRequestSchema = z.object({
|
||||
envelopeId: z.string(),
|
||||
envelopeId: z.string().describe('The ID of the envelope to update.'),
|
||||
data: z
|
||||
.object({
|
||||
title: ZDocumentTitleSchema.optional(),
|
||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||
visibility: ZDocumentVisibilitySchema.optional(),
|
||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||
folderId: z.string().nullish(),
|
||||
templateType: z.nativeEnum(TemplateType).optional(),
|
||||
globalAccessAuth: z
|
||||
.array(ZDocumentAccessAuthTypesSchema)
|
||||
.describe('The authentication methods required to access the envelope.')
|
||||
.optional(),
|
||||
globalActionAuth: z
|
||||
.array(ZDocumentActionAuthTypesSchema)
|
||||
.describe('The authentication methods required to sign the envelope.')
|
||||
.optional(),
|
||||
folderId: z.string().describe('The ID of the folder containing the envelope.').nullish(),
|
||||
templateType: z.nativeEnum(TemplateType).describe('The template type.').optional(),
|
||||
})
|
||||
.describe('The envelope properties to update.')
|
||||
.optional(),
|
||||
meta: ZDocumentMetaUpdateSchema.optional(),
|
||||
meta: ZDocumentMetaUpdateSchema.describe('The email and signing settings to update.').optional(),
|
||||
});
|
||||
|
||||
export const ZUpdateEnvelopeResponseSchema = ZEnvelopeLiteSchema;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { formatSigningLink } from '@documenso/lib/utils/recipients';
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
import { match, P } from 'ts-pattern';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import { useEnvelopeMeta, ZUseEnvelopeRequestSchema, ZUseEnvelopeResponseSchema } from './use-envelope.types';
|
||||
@@ -87,20 +88,11 @@ export const useEnvelopeRoute = authenticatedProcedure
|
||||
|
||||
// Map custom document data using identifiers
|
||||
const customDocumentDataMapped = customDocumentData?.map((mapping) => {
|
||||
let documentDataId: string | undefined;
|
||||
|
||||
// Find the uploaded file by identifier
|
||||
if (typeof mapping.identifier === 'string') {
|
||||
documentDataId = uploadedFiles.find((file) => file.name === mapping.identifier)?.documentDataId;
|
||||
}
|
||||
|
||||
if (typeof mapping.identifier === 'number') {
|
||||
documentDataId = uploadedFiles.at(mapping.identifier)?.documentDataId;
|
||||
}
|
||||
|
||||
if (mapping.identifier === undefined) {
|
||||
documentDataId = uploadedFiles.at(0)?.documentDataId;
|
||||
}
|
||||
const documentDataId = match(mapping.identifier)
|
||||
.with(P.string, (name) => uploadedFiles.find((file) => file.name === name)?.documentDataId)
|
||||
.with(P.number, (index) => uploadedFiles.at(index)?.documentDataId)
|
||||
.exhaustive();
|
||||
|
||||
if (!documentDataId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { getOrganisationAnalyticsDocumentsOverTime } from '@documenso/lib/server-only/organisation/get-organisation-analytics-documents-over-time';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationAnalyticsDocumentsOverTimeRequestSchema,
|
||||
ZGetOrganisationAnalyticsDocumentsOverTimeResponseSchema,
|
||||
} from './get-organisation-analytics.types';
|
||||
|
||||
export const getOrganisationAnalyticsDocumentsOverTimeRoute = authenticatedProcedure
|
||||
.input(ZGetOrganisationAnalyticsDocumentsOverTimeRequestSchema)
|
||||
.output(ZGetOrganisationAnalyticsDocumentsOverTimeResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getOrganisationAnalyticsDocumentsOverTime({
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getOrganisationAnalyticsOverview } from '@documenso/lib/server-only/organisation/get-organisation-analytics-overview';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationAnalyticsOverviewRequestSchema,
|
||||
ZGetOrganisationAnalyticsOverviewResponseSchema,
|
||||
} from './get-organisation-analytics.types';
|
||||
|
||||
export const getOrganisationAnalyticsOverviewRoute = authenticatedProcedure
|
||||
.input(ZGetOrganisationAnalyticsOverviewRequestSchema)
|
||||
.output(ZGetOrganisationAnalyticsOverviewResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getOrganisationAnalyticsOverview({
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { getOrganisationAnalyticsStatusBreakdown } from '@documenso/lib/server-only/organisation/get-organisation-analytics-status-breakdown';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationAnalyticsStatusBreakdownRequestSchema,
|
||||
ZGetOrganisationAnalyticsStatusBreakdownResponseSchema,
|
||||
} from './get-organisation-analytics.types';
|
||||
|
||||
export const getOrganisationAnalyticsStatusBreakdownRoute = authenticatedProcedure
|
||||
.input(ZGetOrganisationAnalyticsStatusBreakdownRequestSchema)
|
||||
.output(ZGetOrganisationAnalyticsStatusBreakdownResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getOrganisationAnalyticsStatusBreakdown({
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { getOrganisationAnalyticsTeamActivity } from '@documenso/lib/server-only/organisation/get-organisation-analytics-team-activity';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationAnalyticsTeamActivityRequestSchema,
|
||||
ZGetOrganisationAnalyticsTeamActivityResponseSchema,
|
||||
} from './get-organisation-analytics.types';
|
||||
|
||||
export const getOrganisationAnalyticsTeamActivityRoute = authenticatedProcedure
|
||||
.input(ZGetOrganisationAnalyticsTeamActivityRequestSchema)
|
||||
.output(ZGetOrganisationAnalyticsTeamActivityResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getOrganisationAnalyticsTeamActivity({
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getOrganisationAnalyticsTemplateUsage } from '@documenso/lib/server-only/organisation/get-organisation-analytics-template-usage';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetOrganisationAnalyticsTemplateUsageRequestSchema,
|
||||
ZGetOrganisationAnalyticsTemplateUsageResponseSchema,
|
||||
} from './get-organisation-analytics.types';
|
||||
|
||||
export const getOrganisationAnalyticsTemplateUsageRoute = authenticatedProcedure
|
||||
.input(ZGetOrganisationAnalyticsTemplateUsageRequestSchema)
|
||||
.output(ZGetOrganisationAnalyticsTemplateUsageResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { organisationId, range, from, to, timezone, limit } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
|
||||
return await getOrganisationAnalyticsTemplateUsage({
|
||||
organisationId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
limit,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,161 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
ZAnalyticsRangeFieldsSchema,
|
||||
ZGetTeamAnalyticsDocumentsOverTimeResponseSchema,
|
||||
ZGetTeamAnalyticsStatusBreakdownResponseSchema,
|
||||
ZTeamAnalyticsResolvedRangeSchema,
|
||||
} from '../team-router/get-team-analytics.types';
|
||||
|
||||
/**
|
||||
* Base request shared by every organisation analytics procedure.
|
||||
*
|
||||
* Organisation analytics are restricted to organisation ADMINs. The organisation
|
||||
* ADMIN role authorises organisation-wide visibility: the internal ADMIN group is
|
||||
* attached to every team by `createTeam` and cannot be detached, so every document
|
||||
* in the organisation is in scope and no per-document visibility filtering is applied.
|
||||
*/
|
||||
export const ZOrganisationAnalyticsRequestSchema = ZAnalyticsRangeFieldsSchema.extend({
|
||||
organisationId: z.string().min(1),
|
||||
});
|
||||
|
||||
export type TOrganisationAnalyticsRequest = z.infer<typeof ZOrganisationAnalyticsRequestSchema>;
|
||||
|
||||
const ZCountSchema = z.number().int().nonnegative();
|
||||
|
||||
const ZAnalyticsTeamSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
avatarImageId: z.string().nullable(),
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// organisation.analytics.getOverview
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetOrganisationAnalyticsOverviewRequestSchema = ZOrganisationAnalyticsRequestSchema;
|
||||
|
||||
export const ZGetOrganisationAnalyticsOverviewResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
/** Documents whose DOCUMENT_SENT event falls inside the window. */
|
||||
sent: z.object({
|
||||
current: ZCountSchema,
|
||||
previous: ZCountSchema,
|
||||
}),
|
||||
/** Of documents sent in the window, how many are currently COMPLETED. */
|
||||
completionRate: z.object({
|
||||
completed: ZCountSchema,
|
||||
sent: ZCountSchema,
|
||||
rate: z.number().min(0).max(100).nullable(),
|
||||
previousRate: z.number().min(0).max(100).nullable(),
|
||||
}),
|
||||
teams: z.object({
|
||||
/** Total number of teams in the organisation. */
|
||||
total: ZCountSchema,
|
||||
/** Teams with at least one document sent in the window. */
|
||||
active: ZCountSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TGetOrganisationAnalyticsOverviewRequest = z.infer<typeof ZGetOrganisationAnalyticsOverviewRequestSchema>;
|
||||
export type TGetOrganisationAnalyticsOverviewResponse = z.infer<typeof ZGetOrganisationAnalyticsOverviewResponseSchema>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// organisation.analytics.getDocumentsOverTime
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetOrganisationAnalyticsDocumentsOverTimeRequestSchema = ZOrganisationAnalyticsRequestSchema;
|
||||
|
||||
export const ZGetOrganisationAnalyticsDocumentsOverTimeResponseSchema =
|
||||
ZGetTeamAnalyticsDocumentsOverTimeResponseSchema;
|
||||
|
||||
export type TGetOrganisationAnalyticsDocumentsOverTimeRequest = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsDocumentsOverTimeRequestSchema
|
||||
>;
|
||||
export type TGetOrganisationAnalyticsDocumentsOverTimeResponse = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsDocumentsOverTimeResponseSchema
|
||||
>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// organisation.analytics.getStatusBreakdown
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetOrganisationAnalyticsStatusBreakdownRequestSchema = ZOrganisationAnalyticsRequestSchema;
|
||||
|
||||
export const ZGetOrganisationAnalyticsStatusBreakdownResponseSchema = ZGetTeamAnalyticsStatusBreakdownResponseSchema;
|
||||
|
||||
export type TGetOrganisationAnalyticsStatusBreakdownRequest = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsStatusBreakdownRequestSchema
|
||||
>;
|
||||
export type TGetOrganisationAnalyticsStatusBreakdownResponse = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsStatusBreakdownResponseSchema
|
||||
>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// organisation.analytics.getTemplateUsage
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetOrganisationAnalyticsTemplateUsageRequestSchema = ZOrganisationAnalyticsRequestSchema.extend({
|
||||
limit: z.number().int().min(1).max(20).default(5),
|
||||
});
|
||||
|
||||
/** Templates across the organisation ranked by documents created from them inside the window. */
|
||||
export const ZGetOrganisationAnalyticsTemplateUsageResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
templates: z.array(
|
||||
z.object({
|
||||
/** Legacy numeric template id (Envelope.templateId on documents). */
|
||||
id: z.number().int().positive(),
|
||||
/** Envelope id of the template, null when the template was deleted. */
|
||||
envelopeId: z.string().nullable(),
|
||||
title: z.string().nullable(),
|
||||
updatedAt: z.date().nullable(),
|
||||
/** Team the template belongs to, null when the template was deleted. */
|
||||
team: ZAnalyticsTeamSchema.nullable(),
|
||||
count: ZCountSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetOrganisationAnalyticsTemplateUsageRequest = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsTemplateUsageRequestSchema
|
||||
>;
|
||||
export type TGetOrganisationAnalyticsTemplateUsageResponse = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsTemplateUsageResponseSchema
|
||||
>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// organisation.analytics.getTeamActivity
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetOrganisationAnalyticsTeamActivityRequestSchema = ZOrganisationAnalyticsRequestSchema;
|
||||
|
||||
/**
|
||||
* Per-team activity for every team in the organisation. Teams with no activity
|
||||
* are included with zeros. Sorted by sent desc, then name asc.
|
||||
*/
|
||||
export const ZGetOrganisationAnalyticsTeamActivityResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
teams: z.array(
|
||||
ZAnalyticsTeamSchema.extend({
|
||||
/** Documents sent by this team inside the window. */
|
||||
sent: ZCountSchema,
|
||||
/** Of `sent`, currently COMPLETED. */
|
||||
completed: ZCountSchema,
|
||||
/** Of `sent`, currently PENDING. */
|
||||
pending: ZCountSchema,
|
||||
/** 0-100, null when `sent` is 0. */
|
||||
completionRate: z.number().min(0).max(100).nullable(),
|
||||
/** Most recent send inside the window, null when none. */
|
||||
lastActiveAt: z.date().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetOrganisationAnalyticsTeamActivityRequest = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsTeamActivityRequestSchema
|
||||
>;
|
||||
export type TGetOrganisationAnalyticsTeamActivityResponse = z.infer<
|
||||
typeof ZGetOrganisationAnalyticsTeamActivityResponseSchema
|
||||
>;
|
||||
@@ -13,6 +13,11 @@ import { findOrganisationGroupsRoute } from './find-organisation-groups';
|
||||
import { findOrganisationMemberInvitesRoute } from './find-organisation-member-invites';
|
||||
import { findOrganisationMembersRoute } from './find-organisation-members';
|
||||
import { getOrganisationRoute } from './get-organisation';
|
||||
import { getOrganisationAnalyticsDocumentsOverTimeRoute } from './get-organisation-analytics-documents-over-time';
|
||||
import { getOrganisationAnalyticsOverviewRoute } from './get-organisation-analytics-overview';
|
||||
import { getOrganisationAnalyticsStatusBreakdownRoute } from './get-organisation-analytics-status-breakdown';
|
||||
import { getOrganisationAnalyticsTeamActivityRoute } from './get-organisation-analytics-team-activity';
|
||||
import { getOrganisationAnalyticsTemplateUsageRoute } from './get-organisation-analytics-template-usage';
|
||||
import { getOrganisationMemberInvitesRoute } from './get-organisation-member-invites';
|
||||
import { getOrganisationQuotaFlagsRoute } from './get-organisation-quota-flags';
|
||||
import { getOrganisationSessionRoute } from './get-organisation-session';
|
||||
@@ -33,6 +38,13 @@ export const organisationRouter = router({
|
||||
update: updateOrganisationRoute,
|
||||
delete: deleteOrganisationRoute,
|
||||
leave: leaveOrganisationRoute,
|
||||
analytics: {
|
||||
getOverview: getOrganisationAnalyticsOverviewRoute,
|
||||
getDocumentsOverTime: getOrganisationAnalyticsDocumentsOverTimeRoute,
|
||||
getStatusBreakdown: getOrganisationAnalyticsStatusBreakdownRoute,
|
||||
getTemplateUsage: getOrganisationAnalyticsTemplateUsageRoute,
|
||||
getTeamActivity: getOrganisationAnalyticsTeamActivityRoute,
|
||||
},
|
||||
member: {
|
||||
find: findOrganisationMembersRoute,
|
||||
update: updateOrganisationMemberRoute,
|
||||
|
||||
@@ -603,7 +603,7 @@ export const recipientRouter = router({
|
||||
// can't complete via this route — they go through the CSC sync sign
|
||||
// flow (`enterprise.csc.signEnvelope`). This route returns the redirect URL
|
||||
// for the credential-scope OAuth round-trip.
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
...unsafeBuildEnvelopeIdQuery({ type: 'documentId', id: documentId }, EnvelopeType.DOCUMENT),
|
||||
recipients: { some: { token } },
|
||||
@@ -611,6 +611,16 @@ export const recipientRouter = router({
|
||||
select: { signatureLevel: true, internalVersion: true },
|
||||
});
|
||||
|
||||
// The most common cause is a stale signing page: the document was
|
||||
// deleted, or the recipient was removed, after the link was opened.
|
||||
// Surface a NOT_FOUND instead of leaking a Prisma P2025 as a 500.
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found for the provided signing token',
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
|
||||
if (isTspEnvelope(envelope)) {
|
||||
return await prepareCscRecipientSigning({
|
||||
recipientToken: token,
|
||||
|
||||
@@ -8,7 +8,7 @@ export const findTeamsRoute = authenticatedProcedure
|
||||
.input(ZFindTeamsRequestSchema)
|
||||
.output(ZFindTeamsResponseSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { organisationId } = input;
|
||||
const { organisationId, query, page, perPage } = input;
|
||||
const { user } = ctx;
|
||||
|
||||
ctx.logger.info({
|
||||
@@ -17,5 +17,5 @@ export const findTeamsRoute = authenticatedProcedure
|
||||
},
|
||||
});
|
||||
|
||||
return findTeams({ userId: user.id, organisationId });
|
||||
return findTeams({ userId: user.id, organisationId, query, page, perPage });
|
||||
});
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getTeamAnalyticsDocumentsOverTime } from '@documenso/lib/server-only/team/get-team-analytics-documents-over-time';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTeamAnalyticsDocumentsOverTimeRequestSchema,
|
||||
ZGetTeamAnalyticsDocumentsOverTimeResponseSchema,
|
||||
} from './get-team-analytics.types';
|
||||
|
||||
export const getTeamAnalyticsDocumentsOverTimeRoute = authenticatedProcedure
|
||||
.input(ZGetTeamAnalyticsDocumentsOverTimeRequestSchema)
|
||||
.output(ZGetTeamAnalyticsDocumentsOverTimeResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getTeamAnalyticsDocumentsOverTime({
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getTeamAnalyticsMemberActivity } from '@documenso/lib/server-only/team/get-team-analytics-member-activity';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTeamAnalyticsMemberActivityRequestSchema,
|
||||
ZGetTeamAnalyticsMemberActivityResponseSchema,
|
||||
} from './get-team-analytics.types';
|
||||
|
||||
export const getTeamAnalyticsMemberActivityRoute = authenticatedProcedure
|
||||
.input(ZGetTeamAnalyticsMemberActivityRequestSchema)
|
||||
.output(ZGetTeamAnalyticsMemberActivityResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getTeamAnalyticsMemberActivity({
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getTeamAnalyticsOverview } from '@documenso/lib/server-only/team/get-team-analytics-overview';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTeamAnalyticsOverviewRequestSchema,
|
||||
ZGetTeamAnalyticsOverviewResponseSchema,
|
||||
} from './get-team-analytics.types';
|
||||
|
||||
export const getTeamAnalyticsOverviewRoute = authenticatedProcedure
|
||||
.input(ZGetTeamAnalyticsOverviewRequestSchema)
|
||||
.output(ZGetTeamAnalyticsOverviewResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getTeamAnalyticsOverview({
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getTeamAnalyticsStatusBreakdown } from '@documenso/lib/server-only/team/get-team-analytics-status-breakdown';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTeamAnalyticsStatusBreakdownRequestSchema,
|
||||
ZGetTeamAnalyticsStatusBreakdownResponseSchema,
|
||||
} from './get-team-analytics.types';
|
||||
|
||||
export const getTeamAnalyticsStatusBreakdownRoute = authenticatedProcedure
|
||||
.input(ZGetTeamAnalyticsStatusBreakdownRequestSchema)
|
||||
.output(ZGetTeamAnalyticsStatusBreakdownResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, range, from, to, timezone } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
},
|
||||
});
|
||||
|
||||
return await getTeamAnalyticsStatusBreakdown({
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getTeamAnalyticsTemplateUsage } from '@documenso/lib/server-only/team/get-team-analytics-template-usage';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZGetTeamAnalyticsTemplateUsageRequestSchema,
|
||||
ZGetTeamAnalyticsTemplateUsageResponseSchema,
|
||||
} from './get-team-analytics.types';
|
||||
|
||||
export const getTeamAnalyticsTemplateUsageRoute = authenticatedProcedure
|
||||
.input(ZGetTeamAnalyticsTemplateUsageRequestSchema)
|
||||
.output(ZGetTeamAnalyticsTemplateUsageResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId, range, from, to, timezone, limit } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
|
||||
return await getTeamAnalyticsTemplateUsage({
|
||||
teamId,
|
||||
range,
|
||||
from,
|
||||
to,
|
||||
timezone,
|
||||
limit,
|
||||
userId: ctx.user.id,
|
||||
userEmail: ctx.user.email,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import { IANAZone } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
/** Preset date ranges supported by analytics, plus a custom [from, to] window. */
|
||||
export const ZTeamAnalyticsRangeSchema = z.enum(['7d', '30d', '90d', '12m', 'custom']);
|
||||
|
||||
export type TTeamAnalyticsRange = z.infer<typeof ZTeamAnalyticsRangeSchema>;
|
||||
|
||||
/** Calendar date in the request timezone, formatted yyyy-MM-dd. */
|
||||
export const ZAnalyticsDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected yyyy-MM-dd');
|
||||
|
||||
/** How far back a custom range may start: one year and one day before today. */
|
||||
export const ANALYTICS_CUSTOM_RANGE_MAX_LOOKBACK = { years: 1, days: 1 } as const;
|
||||
|
||||
/**
|
||||
* Range fields shared by team and organisation analytics requests.
|
||||
*
|
||||
* `from` and `to` are inclusive calendar dates and are required when `range` is
|
||||
* `custom`, ignored otherwise. Combined validation happens in the range resolver so
|
||||
* this object stays extendable.
|
||||
*/
|
||||
export const ZAnalyticsRangeFieldsSchema = z.object({
|
||||
range: ZTeamAnalyticsRangeSchema.default('30d'),
|
||||
from: ZAnalyticsDateSchema.optional(),
|
||||
to: ZAnalyticsDateSchema.optional(),
|
||||
/** IANA timezone used to resolve day boundaries and buckets. */
|
||||
timezone: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine((timezone) => IANAZone.isValidZone(timezone), { message: 'Invalid timezone' })
|
||||
.default('UTC'),
|
||||
});
|
||||
|
||||
export type TAnalyticsRangeFields = z.infer<typeof ZAnalyticsRangeFieldsSchema>;
|
||||
|
||||
/** Base request shared by every team analytics procedure. */
|
||||
export const ZTeamAnalyticsRequestSchema = ZAnalyticsRangeFieldsSchema.extend({
|
||||
teamId: z.number().int().positive(),
|
||||
});
|
||||
|
||||
export type TTeamAnalyticsRequest = z.infer<typeof ZTeamAnalyticsRequestSchema>;
|
||||
|
||||
const ZCountSchema = z.number().int().nonnegative();
|
||||
|
||||
/** Resolved half-open date window, plus the equally sized window immediately before it. */
|
||||
export const ZTeamAnalyticsResolvedRangeSchema = z.object({
|
||||
range: ZTeamAnalyticsRangeSchema,
|
||||
/** Inclusive calendar bounds of the window in the request timezone, yyyy-MM-dd. */
|
||||
from: ZAnalyticsDateSchema,
|
||||
to: ZAnalyticsDateSchema,
|
||||
timezone: z.string(),
|
||||
start: z.date(),
|
||||
end: z.date(),
|
||||
previousStart: z.date(),
|
||||
previousEnd: z.date(),
|
||||
bucket: z.enum(['day', 'month']),
|
||||
});
|
||||
|
||||
export type TTeamAnalyticsResolvedRange = z.infer<typeof ZTeamAnalyticsResolvedRangeSchema>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// team.analytics.getOverview
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetTeamAnalyticsOverviewRequestSchema = ZTeamAnalyticsRequestSchema;
|
||||
|
||||
export const ZGetTeamAnalyticsOverviewResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
/** Documents whose first DOCUMENT_SENT event falls inside the window. */
|
||||
sent: z.object({
|
||||
current: ZCountSchema,
|
||||
previous: ZCountSchema,
|
||||
}),
|
||||
/** Of documents sent in the window, how many are currently COMPLETED. */
|
||||
completionRate: z.object({
|
||||
completed: ZCountSchema,
|
||||
sent: ZCountSchema,
|
||||
/** 0-100, null when nothing was sent. */
|
||||
rate: z.number().min(0).max(100).nullable(),
|
||||
/** Previous window rate, null when nothing was sent then. */
|
||||
previousRate: z.number().min(0).max(100).nullable(),
|
||||
}),
|
||||
members: z.object({
|
||||
/** Total number of users who are members of the team. */
|
||||
total: ZCountSchema,
|
||||
/** Distinct team members who sent at least one visible document in the window. */
|
||||
active: ZCountSchema,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TGetTeamAnalyticsOverviewRequest = z.infer<typeof ZGetTeamAnalyticsOverviewRequestSchema>;
|
||||
export type TGetTeamAnalyticsOverviewResponse = z.infer<typeof ZGetTeamAnalyticsOverviewResponseSchema>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// team.analytics.getDocumentsOverTime
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetTeamAnalyticsDocumentsOverTimeRequestSchema = ZTeamAnalyticsRequestSchema;
|
||||
|
||||
export const ZGetTeamAnalyticsDocumentsOverTimeResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
total: ZCountSchema,
|
||||
/** Zero-filled, ordered ascending. `date` is the bucket start as yyyy-MM-dd in the request timezone. */
|
||||
points: z.array(
|
||||
z.object({
|
||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
count: ZCountSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetTeamAnalyticsDocumentsOverTimeRequest = z.infer<typeof ZGetTeamAnalyticsDocumentsOverTimeRequestSchema>;
|
||||
export type TGetTeamAnalyticsDocumentsOverTimeResponse = z.infer<
|
||||
typeof ZGetTeamAnalyticsDocumentsOverTimeResponseSchema
|
||||
>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// team.analytics.getStatusBreakdown
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetTeamAnalyticsStatusBreakdownRequestSchema = ZTeamAnalyticsRequestSchema;
|
||||
|
||||
/** Current status of documents created inside the window. */
|
||||
export const ZGetTeamAnalyticsStatusBreakdownResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
total: ZCountSchema,
|
||||
draft: ZCountSchema,
|
||||
pending: ZCountSchema,
|
||||
completed: ZCountSchema,
|
||||
rejected: ZCountSchema,
|
||||
cancelled: ZCountSchema,
|
||||
});
|
||||
|
||||
export type TGetTeamAnalyticsStatusBreakdownRequest = z.infer<typeof ZGetTeamAnalyticsStatusBreakdownRequestSchema>;
|
||||
export type TGetTeamAnalyticsStatusBreakdownResponse = z.infer<typeof ZGetTeamAnalyticsStatusBreakdownResponseSchema>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// team.analytics.getTemplateUsage
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetTeamAnalyticsTemplateUsageRequestSchema = ZTeamAnalyticsRequestSchema.extend({
|
||||
limit: z.number().int().min(1).max(20).default(5),
|
||||
});
|
||||
|
||||
/** Templates ranked by the number of visible documents created from them inside the window. */
|
||||
export const ZGetTeamAnalyticsTemplateUsageResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
templates: z.array(
|
||||
z.object({
|
||||
/** Legacy numeric template id (Envelope.templateId on documents). */
|
||||
id: z.number().int().positive(),
|
||||
/** Envelope id of the template, null when the template was deleted or is not visible to the caller. */
|
||||
envelopeId: z.string().nullable(),
|
||||
title: z.string().nullable(),
|
||||
updatedAt: z.date().nullable(),
|
||||
count: ZCountSchema,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetTeamAnalyticsTemplateUsageRequest = z.infer<typeof ZGetTeamAnalyticsTemplateUsageRequestSchema>;
|
||||
export type TGetTeamAnalyticsTemplateUsageResponse = z.infer<typeof ZGetTeamAnalyticsTemplateUsageResponseSchema>;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// team.analytics.getMemberActivity
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export const ZGetTeamAnalyticsMemberActivityRequestSchema = ZTeamAnalyticsRequestSchema;
|
||||
|
||||
/**
|
||||
* Per-member activity for every current team member, scoped to documents the
|
||||
* caller can see. Members with no visible activity are included with zeros.
|
||||
* Sorted by sent desc, then name asc.
|
||||
*/
|
||||
export const ZGetTeamAnalyticsMemberActivityResponseSchema = z.object({
|
||||
range: ZTeamAnalyticsResolvedRangeSchema,
|
||||
members: z.array(
|
||||
z.object({
|
||||
userId: z.number().int().positive(),
|
||||
name: z.string().nullable(),
|
||||
email: z.string(),
|
||||
avatarImageId: z.string().nullable(),
|
||||
/** Visible documents first sent by this member inside the window. */
|
||||
sent: ZCountSchema,
|
||||
/** Of `sent`, currently COMPLETED. */
|
||||
completed: ZCountSchema,
|
||||
/** Of `sent`, currently PENDING. */
|
||||
pending: ZCountSchema,
|
||||
/** 0-100, null when `sent` is 0. */
|
||||
completionRate: z.number().min(0).max(100).nullable(),
|
||||
/** Most recent send inside the window, null when none. */
|
||||
lastActiveAt: z.date().nullable(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type TGetTeamAnalyticsMemberActivityRequest = z.infer<typeof ZGetTeamAnalyticsMemberActivityRequestSchema>;
|
||||
export type TGetTeamAnalyticsMemberActivityResponse = z.infer<typeof ZGetTeamAnalyticsMemberActivityResponseSchema>;
|
||||
@@ -16,6 +16,11 @@ import { findTeamGroupsRoute } from './find-team-groups';
|
||||
import { findTeamMembersRoute } from './find-team-members';
|
||||
import { findTeamsRoute } from './find-teams';
|
||||
import { getTeamRoute } from './get-team';
|
||||
import { getTeamAnalyticsDocumentsOverTimeRoute } from './get-team-analytics-documents-over-time';
|
||||
import { getTeamAnalyticsMemberActivityRoute } from './get-team-analytics-member-activity';
|
||||
import { getTeamAnalyticsOverviewRoute } from './get-team-analytics-overview';
|
||||
import { getTeamAnalyticsStatusBreakdownRoute } from './get-team-analytics-status-breakdown';
|
||||
import { getTeamAnalyticsTemplateUsageRoute } from './get-team-analytics-template-usage';
|
||||
import { getTeamMembersRoute } from './get-team-members';
|
||||
import {
|
||||
ZCreateTeamEmailVerificationMutationSchema,
|
||||
@@ -36,6 +41,13 @@ export const teamRouter = router({
|
||||
create: createTeamRoute,
|
||||
update: updateTeamRoute,
|
||||
delete: deleteTeamRoute,
|
||||
analytics: {
|
||||
getOverview: getTeamAnalyticsOverviewRoute,
|
||||
getDocumentsOverTime: getTeamAnalyticsDocumentsOverTimeRoute,
|
||||
getStatusBreakdown: getTeamAnalyticsStatusBreakdownRoute,
|
||||
getTemplateUsage: getTeamAnalyticsTemplateUsageRoute,
|
||||
getMemberActivity: getTeamAnalyticsMemberActivityRoute,
|
||||
},
|
||||
member: {
|
||||
find: findTeamMembersRoute,
|
||||
getMany: getTeamMembersRoute,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||
import { mapEnvelopeToTemplateMany } from '@documenso/lib/utils/templates';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindTemplatesInternalRequestSchema,
|
||||
ZFindTemplatesInternalResponseSchema,
|
||||
} from './find-templates-internal.types';
|
||||
|
||||
export const findTemplatesInternalRoute = authenticatedProcedure
|
||||
.input(ZFindTemplatesInternalRequestSchema)
|
||||
.output(ZFindTemplatesInternalResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { user, teamId } = ctx;
|
||||
|
||||
const { query, type, folderId, page, perPage, ownerIds } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
folderId,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await findTemplates({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
query,
|
||||
type,
|
||||
folderId,
|
||||
page,
|
||||
perPage,
|
||||
ownerIds,
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data.map((envelope) => mapEnvelopeToTemplateMany(envelope)),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZFindTemplatesRequestSchema, ZFindTemplatesResponseSchema } from './schema';
|
||||
|
||||
export const ZFindTemplatesInternalRequestSchema = ZFindTemplatesRequestSchema.extend({
|
||||
ownerIds: z.array(z.number()).optional(),
|
||||
});
|
||||
|
||||
export const ZFindTemplatesInternalResponseSchema = ZFindTemplatesResponseSchema;
|
||||
|
||||
export type TFindTemplatesInternalRequest = z.infer<typeof ZFindTemplatesInternalRequestSchema>;
|
||||
export type TFindTemplatesInternalResponse = z.infer<typeof ZFindTemplatesInternalResponseSchema>;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { jobs } from '@documenso/lib/jobs/client';
|
||||
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
|
||||
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
|
||||
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
||||
import { convertToPdf } from '@documenso/lib/server-only/document-conversion';
|
||||
@@ -21,17 +22,19 @@ import { findTemplates } from '@documenso/lib/server-only/template/find-template
|
||||
import { getOrganisationTemplateById } from '@documenso/lib/server-only/template/get-organisation-template-by-id';
|
||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
||||
import { validateBulkSendCsv } from '@documenso/lib/server-only/template/validate-bulk-send-csv';
|
||||
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
|
||||
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
|
||||
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
|
||||
import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
|
||||
import { mapEnvelopeToTemplateLite, mapEnvelopeToTemplateMany } from '@documenso/lib/utils/templates';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Envelope } from '@prisma/client';
|
||||
import { DocumentDataType, EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
|
||||
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
|
||||
import { findTemplatesInternalRoute } from './find-templates-internal';
|
||||
import { getTemplatesByIdsRoute } from './get-templates-by-ids';
|
||||
import {
|
||||
ZBulkSendTemplateMutationSchema,
|
||||
@@ -98,35 +101,15 @@ export const templateRouter = router({
|
||||
// Remapping for backwards compatibility.
|
||||
return {
|
||||
...result,
|
||||
data: result.data.map((envelope) => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
id: legacyTemplateId,
|
||||
envelopeId: envelope.id,
|
||||
type: envelope.templateType,
|
||||
visibility: envelope.visibility,
|
||||
externalId: envelope.externalId,
|
||||
title: envelope.title,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
authOptions: envelope.authOptions,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
publicTitle: envelope.publicTitle,
|
||||
publicDescription: envelope.publicDescription,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
team: envelope.team,
|
||||
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
recipients: envelope.recipients.map((recipient) => mapRecipientToLegacyRecipient(recipient, envelope)),
|
||||
templateMeta: envelope.documentMeta,
|
||||
directLink: envelope.directLink,
|
||||
};
|
||||
}),
|
||||
data: result.data.map((envelope) => mapEnvelopeToTemplateMany(envelope)),
|
||||
};
|
||||
}),
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
findTemplatesInternal: findTemplatesInternalRoute,
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -145,32 +128,7 @@ export const templateRouter = router({
|
||||
// Remapping for backwards compatibility.
|
||||
return {
|
||||
...result,
|
||||
data: result.data.map((envelope) => {
|
||||
const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId);
|
||||
|
||||
return {
|
||||
id: legacyTemplateId,
|
||||
envelopeId: envelope.id,
|
||||
type: envelope.templateType,
|
||||
visibility: envelope.visibility,
|
||||
externalId: envelope.externalId,
|
||||
title: envelope.title,
|
||||
userId: envelope.userId,
|
||||
teamId: envelope.teamId,
|
||||
authOptions: envelope.authOptions,
|
||||
createdAt: envelope.createdAt,
|
||||
updatedAt: envelope.updatedAt,
|
||||
publicTitle: envelope.publicTitle,
|
||||
publicDescription: envelope.publicDescription,
|
||||
folderId: envelope.folderId,
|
||||
useLegacyFieldInsertion: envelope.useLegacyFieldInsertion,
|
||||
team: envelope.team,
|
||||
fields: envelope.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
recipients: envelope.recipients.map((recipient) => mapRecipientToLegacyRecipient(recipient, envelope)),
|
||||
templateMeta: envelope.documentMeta,
|
||||
directLink: envelope.directLink,
|
||||
};
|
||||
}),
|
||||
data: result.data.map((envelope) => mapEnvelopeToTemplateMany(envelope)),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -744,7 +702,7 @@ export const templateRouter = router({
|
||||
});
|
||||
}
|
||||
|
||||
return await createTemplateDirectLink({
|
||||
const directLink = await createTemplateDirectLink({
|
||||
userId,
|
||||
teamId,
|
||||
id: {
|
||||
@@ -753,6 +711,25 @@ export const templateRouter = router({
|
||||
},
|
||||
directRecipientId,
|
||||
});
|
||||
|
||||
fireAndForget(async () => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: { id: template.teamId },
|
||||
select: { organisationId: true },
|
||||
});
|
||||
|
||||
captureServerEvent({
|
||||
event: 'App: Template Direct Link Enabled',
|
||||
userId,
|
||||
teamId: template.teamId,
|
||||
organisationId: team?.organisationId,
|
||||
properties: {
|
||||
envelopeId: template.envelopeId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return directLink;
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -857,6 +834,15 @@ export const templateRouter = router({
|
||||
});
|
||||
}
|
||||
|
||||
const csvValidationResult = validateBulkSendCsv({
|
||||
csvContent: csv,
|
||||
recipientCount: template.recipients.length,
|
||||
});
|
||||
|
||||
if (!csvValidationResult.success) {
|
||||
return { success: false as const, error: csvValidationResult.error };
|
||||
}
|
||||
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.bulk-send-template',
|
||||
payload: {
|
||||
@@ -869,6 +855,6 @@ export const templateRouter = router({
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
return { success: true as const };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { captureServerEvent } from '@documenso/lib/server-only/analytics/capture-server-event';
|
||||
import { createWebhook } from '@documenso/lib/server-only/webhooks/create-webhook';
|
||||
import { deleteWebhookById } from '@documenso/lib/server-only/webhooks/delete-webhook-by-id';
|
||||
import { editWebhook } from '@documenso/lib/server-only/webhooks/edit-webhook';
|
||||
import { getWebhookById } from '@documenso/lib/server-only/webhooks/get-webhook-by-id';
|
||||
import { getWebhooksByTeamId } from '@documenso/lib/server-only/webhooks/get-webhooks-by-team-id';
|
||||
import { triggerTestWebhook } from '@documenso/lib/server-only/webhooks/trigger-test-webhook';
|
||||
import { fireAndForget } from '@documenso/lib/universal/fire-and-forget';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { authenticatedProcedure, router } from '../trpc';
|
||||
import { findWebhookCallsRoute } from './find-webhook-calls';
|
||||
@@ -51,7 +54,7 @@ export const webhookRouter = router({
|
||||
createWebhook: authenticatedProcedure.input(ZCreateWebhookRequestSchema).mutation(async ({ input, ctx }) => {
|
||||
const { enabled, eventTriggers, secret, webhookUrl } = input;
|
||||
|
||||
return await createWebhook({
|
||||
const webhook = await createWebhook({
|
||||
enabled,
|
||||
secret,
|
||||
webhookUrl,
|
||||
@@ -59,6 +62,25 @@ export const webhookRouter = router({
|
||||
teamId: ctx.teamId,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
|
||||
fireAndForget(async () => {
|
||||
const team = await prisma.team.findFirst({
|
||||
where: { id: ctx.teamId },
|
||||
select: { organisationId: true },
|
||||
});
|
||||
|
||||
captureServerEvent({
|
||||
event: 'App: Webhook Created',
|
||||
userId: ctx.user.id,
|
||||
teamId: ctx.teamId,
|
||||
organisationId: team?.organisationId,
|
||||
properties: {
|
||||
triggerCount: eventTriggers.length,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
return webhook;
|
||||
}),
|
||||
|
||||
deleteWebhook: authenticatedProcedure.input(ZDeleteWebhookRequestSchema).mutation(async ({ input, ctx }) => {
|
||||
|
||||
Reference in New Issue
Block a user