diff --git a/apps/web/src/app/(share)/api/share/route.tsx b/apps/web/src/app/(share)/api/share/route.tsx deleted file mode 100644 index d7984db84..000000000 --- a/apps/web/src/app/(share)/api/share/route.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { ImageResponse } from 'next/server'; - -export const config = { - runtime: 'edge', -}; - -export async function GET() { - const [imageData, fontData] = await Promise.all([ - fetch(new URL('../../../../assets/background-pattern.png', import.meta.url)).then((res) => - res.arrayBuffer(), - ), - fetch(new URL('../../../assets/Caveat-Regular.ttf', import.meta.url)).then((res) => - res.arrayBuffer(), - ), - ]); - - return new ImageResponse( - ( -
-
-
Duncan
-
-
- ), - { - width: 1200, - height: 630, - fonts: [ - { - name: 'Caveat', - data: fontData, - style: 'italic', - }, - ], - }, - ); -} diff --git a/apps/web/src/app/(share)/share/[shortId]/page.tsx b/apps/web/src/app/(share)/share/[shortId]/page.tsx new file mode 100644 index 000000000..0c6fb0897 --- /dev/null +++ b/apps/web/src/app/(share)/share/[shortId]/page.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export type SharePageProps = { + params: { + shortId?: string; + }; +}; + +export default async function SharePage({ params: { shortId } }: SharePageProps) { + console.log(shortId); + + return ( +
+

Share Page

+
+ ); +} diff --git a/apps/web/src/app/(share)/share/page.tsx b/apps/web/src/app/(share)/share/page.tsx deleted file mode 100644 index a0309503c..000000000 --- a/apps/web/src/app/(share)/share/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -export default async function SharePage() { - return ( -
-

Share Page

-
- ); -} diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index 71a368da5..9b3b80851 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -1,16 +1,16 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; -import { CheckCircle2, Clock8, Share } from 'lucide-react'; +import { CheckCircle2, Clock8 } from 'lucide-react'; import { match } from 'ts-pattern'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { DocumentStatus, FieldType } from '@documenso/prisma/client'; -import { Button } from '@documenso/ui/primitives/button'; import { DownloadButton } from './download-button'; +import { ShareButton } from './share-button'; import { SigningCard } from './signing-card'; export type CompletedSigningPageProps = { @@ -88,11 +88,7 @@ export default async function CompletedSigningPage({ ))}
- {/* TODO: Hook this up */} - + & { + recipientId: number; + documentId: number; +}; + +export const ShareButton = ({ recipientId, documentId }: ShareButtonProps) => { + const { mutateAsync: createShareId } = trpc.share.create.useMutation(); + + const router = useRouter(); + + return ( + + ); +}; diff --git a/packages/lib/server-only/share/create-share-id.ts b/packages/lib/server-only/share/create-share-id.ts new file mode 100644 index 000000000..0d376e283 --- /dev/null +++ b/packages/lib/server-only/share/create-share-id.ts @@ -0,0 +1,20 @@ +import { nanoid } from 'nanoid'; + +import { prisma } from '@documenso/prisma'; + +export interface CreateSharingIdOptions { + documentId: number; + recipientId: number; +} + +export const createSharingId = async ({ documentId, recipientId }: CreateSharingIdOptions) => { + const result = await prisma.share.create({ + data: { + recipientId, + documentId, + link: nanoid(), + }, + }); + + return result; +}; diff --git a/packages/prisma/migrations/20230829165148_user_sharing_link/migration.sql b/packages/prisma/migrations/20230829165148_user_sharing_link/migration.sql new file mode 100644 index 000000000..793d125b3 --- /dev/null +++ b/packages/prisma/migrations/20230829165148_user_sharing_link/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "Share" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "link" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "documentId" INTEGER, + + CONSTRAINT "Share_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Share_link_key" ON "Share"("link"); + +-- AddForeignKey +ALTER TABLE "Share" ADD CONSTRAINT "Share_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Share" ADD CONSTRAINT "Share_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "Document"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20230829180915_recipient_id_for_user_id/migration.sql b/packages/prisma/migrations/20230829180915_recipient_id_for_user_id/migration.sql new file mode 100644 index 000000000..51008f8ec --- /dev/null +++ b/packages/prisma/migrations/20230829180915_recipient_id_for_user_id/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `userId` on the `Share` table. All the data in the column will be lost. + - Added the required column `recipientId` to the `Share` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Share" DROP CONSTRAINT "Share_userId_fkey"; + +-- AlterTable +ALTER TABLE "Share" DROP COLUMN "userId", +ADD COLUMN "recipientId" INTEGER NOT NULL; + +-- AddForeignKey +ALTER TABLE "Share" ADD CONSTRAINT "Share_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index b2b86f00a..95b2581ff 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -101,33 +101,16 @@ enum DocumentStatus { } model Document { - id Int @id @default(autoincrement()) - userId Int - User User @relation(fields: [userId], references: [id], onDelete: Cascade) - title String - status DocumentStatus @default(DRAFT) - Recipient Recipient[] - Field Field[] - documentDataId String - documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@unique([documentDataId]) -} - -enum DocumentDataType { - S3_PATH - BYTES - BYTES_64 -} - -model DocumentData { - id String @id @default(cuid()) - type DocumentDataType - data String - initialData String - Document Document? + id Int @id @default(autoincrement()) + created DateTime @default(now()) + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + title String + status DocumentStatus @default(DRAFT) + document String + Recipient Recipient[] + Field Field[] + Share Share[] } enum ReadStatus { @@ -159,6 +142,7 @@ model Recipient { Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) Field Field[] Signature Signature[] + Share Share[] @@unique([documentId, email]) } @@ -201,11 +185,13 @@ model Signature { Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict) } -model PasswordResetToken { - id Int @id @default(autoincrement()) - token String @unique - createdAt DateTime @default(now()) - expiry DateTime - userId Int - User User @relation(fields: [userId], references: [id]) +model Share { + id Int @id @default(autoincrement()) + recipientId Int + recipent Recipient @relation(fields: [recipientId], references: [id]) + link String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + document Document? @relation(fields: [documentId], references: [id]) + documentId Int? } diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index afe3b2b07..d69505d08 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -3,6 +3,7 @@ import { authRouter } from './auth-router/router'; import { documentRouter } from './document-router/router'; import { fieldRouter } from './field-router/router'; import { profileRouter } from './profile-router/router'; +import { shareRouter } from './share-router/router'; import { procedure, router } from './trpc'; export const appRouter = router({ @@ -11,7 +12,7 @@ export const appRouter = router({ profile: profileRouter, document: documentRouter, field: fieldRouter, - admin: adminRouter, + share: shareRouter, }); export type AppRouter = typeof appRouter; diff --git a/packages/trpc/server/share-router/router.ts b/packages/trpc/server/share-router/router.ts new file mode 100644 index 000000000..d9001201c --- /dev/null +++ b/packages/trpc/server/share-router/router.ts @@ -0,0 +1,23 @@ +import { TRPCError } from '@trpc/server'; + +import { createSharingId } from '@documenso/lib/server-only/share/create-share-id'; + +import { procedure, router } from '../trpc'; +import { ZShareLinkSchema } from './schema'; + +export const shareRouter = router({ + create: procedure.input(ZShareLinkSchema).mutation(async ({ input }) => { + try { + const { documentId, recipientId } = input; + + return await createSharingId({ documentId, recipientId }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to create a sharing link.', + }); + } + }), +}); diff --git a/packages/trpc/server/share-router/schema.ts b/packages/trpc/server/share-router/schema.ts new file mode 100644 index 000000000..1e063b7b9 --- /dev/null +++ b/packages/trpc/server/share-router/schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const ZShareLinkSchema = z.object({ + documentId: z.number(), + recipientId: z.number(), +}); + +export type ZShareLinkSchema = z.infer;