Skip to content

Commit

Permalink
feat: add esign audit logs (#287)
Browse files Browse the repository at this point in the history
* feat: add esign schema

* feat: add all esign procedure

* feat: add draft only query feature

* feat: add procedure

* feat: esign audit sdk

* feat: add audit

* feat: add view option

* feat: add template detail view

* feat: add gap
  • Loading branch information
G3root authored May 2, 2024
1 parent c65d0d2 commit 5341b63
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 3 deletions.
25 changes: 25 additions & 0 deletions prisma/migrations/20240501122457_esign_logs/migration.sql
Original file line number Diff line number Diff line change
@@ -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");
27 changes: 27 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ model Company {
safes Safe[]
convertibleNotes ConvertibleNote[]
dataRooms DataRoom[]
eSignAudits EsignAudit[]
@@unique([publicId])
}
Expand Down Expand Up @@ -491,6 +492,7 @@ model Template {
eSignRecipient EsignRecipient[]
completedOn DateTime?
eSignAudits EsignAudit[]
@@index([bucketId])
@@index([uploaderId])
Expand Down Expand Up @@ -519,6 +521,7 @@ model EsignRecipient {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
templateFields TemplateField[]
eSignAudits EsignAudit[]
@@index([memberId])
@@index([templateId])
Expand Down Expand Up @@ -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])
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const EsignTemplateDetailPage = async ({
const { name, status, url, fields, recipients } =
await api.template.get.query({
publicId: templatePublicId,
isDraftOnly: true,
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ export const ESignTable = ({ documents, companyPublicId }: ESignTableProps) => {
{item.completedOn ? "Signed" : "Not Signed"}
</TableCell>

<TableCell>
<TableCell className="flex gap-x-2">
<Link
className={buttonVariants()}
href={`/${companyPublicId}/documents/esign/v/${item.publicId}`}
>
View
</Link>

{item.status === "DRAFT" && (
<Link
className={buttonVariants()}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { PdfCanvas } from "@/components/template/pdf-canvas";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { TemplateSigningFieldProvider } from "@/providers/template-signing-field-provider";
import { api } from "@/trpc/server";
import { RiCheckFill } from "@remixicon/react";

export default async function TemplateDetailViewPage({
params: { templatePublicId },
}: {
params: { templatePublicId: string };
}) {
const [{ url, fields }, { audits }] = await Promise.all([
api.template.get.query({
publicId: templatePublicId,
isDraftOnly: false,
}),
api.audit.allEsignAudits.query({
templatePublicId: templatePublicId,
}),
]);

return (
<TemplateSigningFieldProvider fields={fields}>
<div className="flex min-h-screen bg-gray-50">
<div className="flex h-full flex-grow flex-col">
<div className="mx-auto min-h-full w-full px-5 py-10 lg:px-8 2xl:max-w-screen-xl">
<div className="grid grid-cols-12">
<PdfCanvas mode="readonly" url={url} />
</div>
</div>
</div>
<div className="sticky top-0 flex min-h-full w-80 flex-col lg:border-l">
<Card className="border-none bg-transparent shadow-none">
<CardHeader>
<CardTitle>eSigning activity logs</CardTitle>
</CardHeader>
<CardContent>
{audits.length ? (
<div className="flex flex-col gap-y-3">
{audits.map((item) => (
<div className="flex items-start gap-x-2" key={item.id}>
<div>
<div className="rounded-full bg-green-700 ">
<RiCheckFill className="h-5 w-5 text-white" />
</div>
</div>
<p className="break-words text-sm font-medium text-primary/80">
{item.summary}
</p>
</div>
))}
</div>
) : (
<Alert>
<AlertDescription>No logs to show</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
</div>
</div>
</TemplateSigningFieldProvider>
);
}
18 changes: 17 additions & 1 deletion src/server/audit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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,
};
18 changes: 18 additions & 0 deletions src/server/audit/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof EsignAuditSchema>;
34 changes: 34 additions & 0 deletions src/trpc/routers/audit-router/procedures/all-esign-audits.ts
Original file line number Diff line number Diff line change
@@ -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 };
});
3 changes: 3 additions & 0 deletions src/trpc/routers/audit-router/router.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -23,4 +24,6 @@ export const auditRouter = createTRPCRouter({

return { data };
}),

allEsignAudits: allEsignAuditsProcedure,
});
4 changes: 4 additions & 0 deletions src/trpc/routers/audit-router/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ export const ZodGetAuditsQuerySchema = z
export type TypeZodGetAuditsQuerySchema = z.infer<
typeof ZodGetAuditsQuerySchema
>;

export const ZodAllEsignAuditsQuerySchema = z.object({
templatePublicId: z.string(),
});
3 changes: 2 additions & 1 deletion src/trpc/routers/template-router/procedures/get-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const getTemplateProcedure = withAuth
where: {
publicId: input.publicId,
companyId: companyId,
status: "DRAFT",
...(input.isDraftOnly && { status: "DRAFT" }),
},
select: {
name: true,
Expand All @@ -39,6 +39,7 @@ export const getTemplateProcedure = withAuth
viewportWidth: true,
page: true,
recipientId: true,
prefilledValue: true,
},
orderBy: {
top: "asc",
Expand Down
Loading

0 comments on commit 5341b63

Please sign in to comment.