diff --git a/prisma/migrations/20240501122457_esign_logs/migration.sql b/prisma/migrations/20240501122457_esign_logs/migration.sql new file mode 100644 index 000000000..ec95bcba7 --- /dev/null +++ b/prisma/migrations/20240501122457_esign_logs/migration.sql @@ -0,0 +1,25 @@ +-- CreateTable +CREATE TABLE "EsignAudit" ( + "id" TEXT NOT NULL, + "companyId" TEXT NOT NULL, + "templateId" TEXT NOT NULL, + "recipientId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "ip" TEXT NOT NULL, + "userAgent" TEXT NOT NULL, + "location" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "occurredAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EsignAudit_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "EsignAudit_companyId_idx" ON "EsignAudit"("companyId"); + +-- CreateIndex +CREATE INDEX "EsignAudit_templateId_idx" ON "EsignAudit"("templateId"); + +-- CreateIndex +CREATE INDEX "EsignAudit_recipientId_idx" ON "EsignAudit"("recipientId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 85e979d78..e3d56f1ac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -110,6 +110,7 @@ model Company { safes Safe[] convertibleNotes ConvertibleNote[] dataRooms DataRoom[] + eSignAudits EsignAudit[] @@unique([publicId]) } @@ -491,6 +492,7 @@ model Template { eSignRecipient EsignRecipient[] completedOn DateTime? + eSignAudits EsignAudit[] @@index([bucketId]) @@index([uploaderId]) @@ -519,6 +521,7 @@ model EsignRecipient { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt templateFields TemplateField[] + eSignAudits EsignAudit[] @@index([memberId]) @@index([templateId]) @@ -844,3 +847,27 @@ model UpdateRecipient { @@index([updateId]) @@index([stakeholderId]) } + +model EsignAudit { + id String @id @default(cuid()) + + companyId String + company Company @relation(fields: [companyId], references: [id]) + templateId String + template Template @relation(fields: [templateId], references: [id]) + recipientId String + recipient EsignRecipient @relation(fields: [recipientId], references: [id]) + + action String + ip String + userAgent String + location String + summary String + + occurredAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([companyId]) + @@index([templateId]) + @@index([recipientId]) +} diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx index 565d01001..e5ef7860b 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/[templatePublicId]/page.tsx @@ -13,6 +13,7 @@ const EsignTemplateDetailPage = async ({ const { name, status, url, fields, recipients } = await api.template.get.query({ publicId: templatePublicId, + isDraftOnly: true, }); return ( diff --git a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/components/table.tsx b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/components/table.tsx index 53d4d4f35..9921e9d64 100644 --- a/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/components/table.tsx +++ b/src/app/(authenticated)/(dashboard)/[publicId]/documents/esign/components/table.tsx @@ -46,7 +46,14 @@ export const ESignTable = ({ documents, companyPublicId }: ESignTableProps) => { {item.completedOn ? "Signed" : "Not Signed"} - + + + View + + {item.status === "DRAFT" && ( +
+
+
+
+ +
+
+
+
+ + + eSigning activity logs + + + {audits.length ? ( +
+ {audits.map((item) => ( +
+
+
+ +
+
+

+ {item.summary} +

+
+ ))} +
+ ) : ( + + No logs to show + + )} +
+
+
+
+ + ); +} diff --git a/src/server/audit/index.ts b/src/server/audit/index.ts index 31d5820f6..806f510ed 100644 --- a/src/server/audit/index.ts +++ b/src/server/audit/index.ts @@ -24,7 +24,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { type AuditSchemaType } from "@/server/audit/schema"; +import { + type AuditSchemaType, + type TEsignAuditSchema, +} from "@/server/audit/schema"; import { type TPrismaOrTransaction } from "@/server/db"; const create = (data: AuditSchemaType, tx: TPrismaOrTransaction) => { @@ -36,3 +39,16 @@ const create = (data: AuditSchemaType, tx: TPrismaOrTransaction) => { export const Audit = { create, }; + +const esignAuditCreate = ( + data: TEsignAuditSchema, + tx: TPrismaOrTransaction, +) => { + return tx.esignAudit.create({ + data, + }); +}; + +export const EsignAudit = { + create: esignAuditCreate, +}; diff --git a/src/server/audit/schema.ts b/src/server/audit/schema.ts index cede03e81..07b773dc9 100644 --- a/src/server/audit/schema.ts +++ b/src/server/audit/schema.ts @@ -69,3 +69,21 @@ export const getActions = () => { value: action, })); }; + +export const EsignAuditSchema = z.object({ + action: z.enum([ + "document.complete", + "recipient.signed", + "document.email.sent", + ]), + occurredAt: z.date().optional(), + templateId: z.string(), + recipientId: z.string(), + companyId: z.string(), + ip: z.string(), + userAgent: z.string(), + location: z.string(), + summary: z.string(), +}); + +export type TEsignAuditSchema = z.infer; diff --git a/src/trpc/routers/audit-router/procedures/all-esign-audits.ts b/src/trpc/routers/audit-router/procedures/all-esign-audits.ts new file mode 100644 index 000000000..5d28dacbe --- /dev/null +++ b/src/trpc/routers/audit-router/procedures/all-esign-audits.ts @@ -0,0 +1,34 @@ +import { checkMembership } from "@/server/auth"; +import { withAuth } from "@/trpc/api/trpc"; +import { ZodAllEsignAuditsQuerySchema } from "../schema"; + +export const allEsignAuditsProcedure = withAuth + .input(ZodAllEsignAuditsQuerySchema) + .query(async ({ ctx, input }) => { + const { db, session } = ctx; + const { templatePublicId } = input; + + const { audits } = await db.$transaction(async (tx) => { + const { companyId } = await checkMembership({ session, tx }); + + const { id: templateId } = await tx.template.findFirstOrThrow({ + where: { + publicId: templatePublicId, + }, + select: { + id: true, + }, + }); + + const audits = await tx.esignAudit.findMany({ + where: { + companyId, + templateId, + }, + }); + + return { audits }; + }); + + return { audits }; + }); diff --git a/src/trpc/routers/audit-router/router.ts b/src/trpc/routers/audit-router/router.ts index 9ed3d8258..d4cf420b9 100644 --- a/src/trpc/routers/audit-router/router.ts +++ b/src/trpc/routers/audit-router/router.ts @@ -1,5 +1,6 @@ import { checkMembership } from "@/server/auth"; import { createTRPCRouter, withAuth } from "@/trpc/api/trpc"; +import { allEsignAuditsProcedure } from "./procedures/all-esign-audits"; import { ZodGetAuditsQuerySchema } from "./schema"; export const auditRouter = createTRPCRouter({ @@ -23,4 +24,6 @@ export const auditRouter = createTRPCRouter({ return { data }; }), + + allEsignAudits: allEsignAuditsProcedure, }); diff --git a/src/trpc/routers/audit-router/schema.ts b/src/trpc/routers/audit-router/schema.ts index 3ee603257..1451d6f51 100644 --- a/src/trpc/routers/audit-router/schema.ts +++ b/src/trpc/routers/audit-router/schema.ts @@ -10,3 +10,7 @@ export const ZodGetAuditsQuerySchema = z export type TypeZodGetAuditsQuerySchema = z.infer< typeof ZodGetAuditsQuerySchema >; + +export const ZodAllEsignAuditsQuerySchema = z.object({ + templatePublicId: z.string(), +}); diff --git a/src/trpc/routers/template-router/procedures/get-template.ts b/src/trpc/routers/template-router/procedures/get-template.ts index 87584c975..1a5dccc2a 100644 --- a/src/trpc/routers/template-router/procedures/get-template.ts +++ b/src/trpc/routers/template-router/procedures/get-template.ts @@ -13,7 +13,7 @@ export const getTemplateProcedure = withAuth where: { publicId: input.publicId, companyId: companyId, - status: "DRAFT", + ...(input.isDraftOnly && { status: "DRAFT" }), }, select: { name: true, @@ -39,6 +39,7 @@ export const getTemplateProcedure = withAuth viewportWidth: true, page: true, recipientId: true, + prefilledValue: true, }, orderBy: { top: "asc", diff --git a/src/trpc/routers/template-router/procedures/sign-template.ts b/src/trpc/routers/template-router/procedures/sign-template.ts index 334c3f809..44bb4a153 100644 --- a/src/trpc/routers/template-router/procedures/sign-template.ts +++ b/src/trpc/routers/template-router/procedures/sign-template.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/prefer-for-of */ +import { dayjsExt } from "@/common/dayjs"; import { getFileFromS3, uploadFile } from "@/common/uploads"; import { generateRange, type Range } from "@/lib/pdf-positioning"; +import { EsignAudit } from "@/server/audit"; import { type PrismaTransactionalClient } from "@/server/db"; import { publicProcedure, type CreateTRPCContextType } from "@/trpc/api/trpc"; import { PDFDocument, StandardFonts } from "pdf-lib"; @@ -59,6 +61,20 @@ export const signTemplateProcedure = publicProcedure } } + await EsignAudit.create( + { + action: "recipient.signed", + companyId: template.companyId, + recipientId: recipient.id, + templateId: template.id, + ip: ctx.requestIp, + location: "", + userAgent: ctx.userAgent, + summary: `${recipient.name ? recipient.name : ""} signed "${template.name}" on ${ctx.userAgent} at ${dayjsExt(new Date()).format("lll")}`, + }, + tx, + ); + const signableRecepients = await tx.esignRecipient.count({ where: { templateId: template.id, @@ -95,6 +111,34 @@ export const signTemplateProcedure = publicProcedure }, }); + await EsignAudit.create( + { + action: "recipient.signed", + companyId: template.companyId, + recipientId: recipient.id, + templateId: template.id, + ip: ctx.requestIp, + location: "", + userAgent: ctx.userAgent, + summary: `${recipient.name ? recipient.name : ""} signed "${template.name}" on ${ctx.userAgent} at ${dayjsExt(new Date()).format("lll")}`, + }, + tx, + ); + + await EsignAudit.create( + { + action: "document.complete", + companyId: template.companyId, + recipientId: recipient.id, + templateId: template.id, + ip: ctx.requestIp, + location: "", + userAgent: ctx.userAgent, + summary: `"${template.name}" completely signed at ${dayjsExt(new Date()).format("lll")}`, + }, + tx, + ); + await signPdf({ bucketKey, companyId, @@ -115,6 +159,34 @@ export const signTemplateProcedure = publicProcedure }, }); + await EsignAudit.create( + { + action: "recipient.signed", + companyId: template.companyId, + recipientId: recipient.id, + templateId: template.id, + ip: ctx.requestIp, + location: "", + userAgent: ctx.userAgent, + summary: `${recipient.name ? recipient.name : ""} signed "${template.name}" on ${ctx.userAgent} at ${dayjsExt(new Date()).format("lll")}`, + }, + tx, + ); + + await EsignAudit.create( + { + action: "document.complete", + companyId: template.companyId, + recipientId: recipient.id, + templateId: template.id, + ip: ctx.requestIp, + location: "", + userAgent: ctx.userAgent, + summary: `"${template.name}" completely signed at ${dayjsExt(new Date()).format("lll")}`, + }, + tx, + ); + await signPdf({ bucketKey, companyId, @@ -144,6 +216,22 @@ export const signTemplateProcedure = publicProcedure }); const email = nextDelivery.email; + const uploaderName = template.uploader.user.name; + + await EsignAudit.create( + { + action: "document.email.sent", + companyId: template.companyId, + recipientId: recipient.id, + templateId: template.id, + ip: ctx.requestIp, + location: "", + userAgent: ctx.userAgent, + summary: `${uploaderName ? uploaderName : ""} sent "${template.name}" to ${recipient.name ? recipient.name : ""} for eSignature at ${dayjsExt(new Date()).format("lll")}`, + }, + tx, + ); + await SendEsignEmail({ token, email }); } } @@ -171,6 +259,15 @@ function getTemplate({ tx, templateId }: getTemplateOptions) { id: true, name: true, orderedDelivery: true, + uploader: { + select: { + user: { + select: { + name: true, + }, + }, + }, + }, }, }); } diff --git a/src/trpc/routers/template-router/schema.ts b/src/trpc/routers/template-router/schema.ts index 0b2b802ac..a0f6f1c55 100644 --- a/src/trpc/routers/template-router/schema.ts +++ b/src/trpc/routers/template-router/schema.ts @@ -14,6 +14,7 @@ export const ZodCreateTemplateMutationSchema = z.object({ export const ZodGetTemplateQuerySchema = z.object({ publicId: z.string(), + isDraftOnly: z.boolean(), }); export const ZodSignTemplateMutationSchema = z.object({