Skip to content

Commit

Permalink
feat: create from template
Browse files Browse the repository at this point in the history
  • Loading branch information
Mythie committed Feb 20, 2024
1 parent 4c5b910 commit b9e5905
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 71 deletions.
112 changes: 53 additions & 59 deletions apps/web/src/components/forms/token.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useEffect, useState } from 'react';
import { useState } from 'react';

import { useRouter } from 'next/navigation';

Expand Down Expand Up @@ -72,16 +72,6 @@ export const ApiTokenForm = ({ className }: ApiTokenFormProps) => {
},
});

useEffect(() => {
if (newlyCreatedToken) {
const timer = setTimeout(() => {
setNewlyCreatedToken('');
}, 30000);

return () => clearTimeout(timer);
}
}, [newlyCreatedToken]);

const copyToken = async (token: string) => {
try {
const copied = await copy(token);
Expand Down Expand Up @@ -166,55 +156,59 @@ export const ApiTokenForm = ({ className }: ApiTokenFormProps) => {
)}
/>

<FormField
control={form.control}
name="expirationDate"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="text-muted-foreground">Token expiration date</FormLabel>

<div className="flex items-center gap-x-4">
<FormControl className="flex-1">
<Select onValueChange={field.onChange} disabled={noExpirationDate}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Choose..." />
</SelectTrigger>
<SelectContent>
{Object.entries(EXPIRATION_DATES).map(([key, date]) => (
<SelectItem key={key} value={key}>
{date}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex flex-col gap-4 md:flex-row">
<FormField
control={form.control}
name="expirationDate"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="text-muted-foreground">Token expiration date</FormLabel>

<div className="flex items-center gap-x-4">
<FormControl className="flex-1">
<Select onValueChange={field.onChange} disabled={noExpirationDate}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Choose..." />
</SelectTrigger>
<SelectContent>
{Object.entries(EXPIRATION_DATES).map(([key, date]) => (
<SelectItem key={key} value={key}>
{date}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
</div>

<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="">
<FormLabel className="text-muted-foreground mt-2">Never expire</FormLabel>
<FormControl>
<div className="block md:py-1.5">
<Switch
className="bg-background"
checked={field.value}
onCheckedChange={(val) => {
setNoExpirationDate((prev) => !prev);
field.onChange(val);
}}
/>
</div>
</FormControl>
</div>

<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex items-center gap-2">
<FormLabel className="text-muted-foreground mt-2">Never expire</FormLabel>
<FormControl>
<Switch
className="bg-background"
checked={field.value}
onCheckedChange={(val) => {
setNoExpirationDate((prev) => !prev);
field.onChange(val);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
</FormItem>
)}
/>
</div>

<Button
type="submit"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/api/v1/[...ts-rest].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ApiContractV1 } from '@documenso/api/v1/contract';
import { ApiContractV1Implementation } from '@documenso/api/v1/implementation';

const nextRouteHandler = createNextRouter(ApiContractV1, ApiContractV1Implementation, {
responseValidation: false,
responseValidation: true,
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
Expand Down
17 changes: 16 additions & 1 deletion packages/api/v1/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { initContract } from '@ts-rest/core';
import {
ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema,
ZAuthorizationHeadersSchema,
ZCreateDocumentFromTemplateMutationResponseSchema,
ZCreateDocumentFromTemplateMutationSchema,
ZCreateDocumentMutationResponseSchema,
ZCreateDocumentMutationSchema,
ZCreateFieldMutationSchema,
Expand All @@ -13,6 +15,7 @@ import {
ZGetDocumentsQuerySchema,
ZSuccessfulDocumentResponseSchema,
ZSuccessfulFieldResponseSchema,
ZSuccessfulGetDocumentResponseSchema,
ZSuccessfulRecipientResponseSchema,
ZSuccessfulResponseSchema,
ZSuccessfulSigningResponseSchema,
Expand Down Expand Up @@ -41,7 +44,7 @@ export const ApiContractV1 = c.router(
method: 'GET',
path: '/api/v1/documents/:id',
responses: {
200: ZSuccessfulDocumentResponseSchema,
200: ZSuccessfulGetDocumentResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
},
Expand All @@ -60,6 +63,18 @@ export const ApiContractV1 = c.router(
summary: 'Upload a new document and get a presigned URL',
},

createDocumentFromTemplate: {
method: 'POST',
path: '/api/v1/templates/:templateId/create-document',
body: ZCreateDocumentFromTemplateMutationSchema,
responses: {
200: ZCreateDocumentFromTemplateMutationResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
},
summary: 'Upload a new document and get a presigned URL',
},

sendDocument: {
method: 'POST',
path: '/api/v1/documents/:id/send',
Expand Down
64 changes: 63 additions & 1 deletion packages/api/v1/implementation.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { createNextRoute } from '@ts-rest/next';

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 { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { createField } from '@documenso/lib/server-only/field/create-field';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
Expand All @@ -15,6 +17,7 @@ import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recip
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';

Expand Down Expand Up @@ -42,10 +45,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {

try {
const document = await getDocumentById({ id: Number(documentId), userId: user.id });
const recipients = await getRecipientsForDocument({
documentId: Number(documentId),
userId: user.id,
});

return {
status: 200,
body: document,
body: {
...document,
recipients,
},
};
} catch (err) {
return {
Expand Down Expand Up @@ -124,6 +134,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
documentId: document.id,
recipients: recipients.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
})),
Expand All @@ -139,6 +151,53 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}
}),

createDocumentFromTemplate: authenticatedMiddleware(async (args, user) => {
const { body, params } = args;

const templateId = Number(params.templateId);

const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;

const document = await createDocumentFromTemplate({
templateId,
userId: user.id,
recipients: body.recipients,
});

await updateDocument({
documentId: document.id,
userId: user.id,
data: {
title: body.title,
},
});

if (body.meta) {
await upsertDocumentMeta({
documentId: document.id,
userId: user.id,
subject: body.meta.subject,
message: body.meta.message,
dateFormat: body.meta.dateFormat,
timezone: body.meta.timezone,
});
}

return {
status: 200,
body: {
documentId: document.id,
recipients: document.Recipient.map((recipient) => ({
recipientId: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
})),
},
};
}),

sendDocument: authenticatedMiddleware(async (args, user) => {
const { id } = args.params;

Expand Down Expand Up @@ -461,6 +520,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
});

const remappedField = {
id: field.id,
documentId: field.documentId,
recipientId: field.recipientId ?? -1,
type: field.type,
Expand Down Expand Up @@ -545,6 +605,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
});

const remappedField = {
id: updatedField.id,
documentId: updatedField.documentId,
recipientId: updatedField.recipientId ?? -1,
type: updatedField.type,
Expand Down Expand Up @@ -635,6 +696,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}

const remappedField = {
id: deletedField.id,
documentId: deletedField.documentId,
recipientId: deletedField.recipientId ?? -1,
type: deletedField.type,
Expand Down
6 changes: 5 additions & 1 deletion packages/api/v1/middleware/authenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ export const authenticatedMiddleware = <
) => {
return async (args: T) => {
try {
const { authorization: token } = args.req.headers;
const { authorization } = args.req.headers;

// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
const [token] = (authorization || '').split('Bearer ').filter((s) => s.length > 0);

if (!token) {
throw new Error('Token was not provided for authenticated middleware');
Expand All @@ -26,6 +29,7 @@ export const authenticatedMiddleware = <

return await handler(args, user);
} catch (_err) {
console.log({ _err });
return {
status: 401,
body: {
Expand Down
Loading

0 comments on commit b9e5905

Please sign in to comment.