feat: create sharing id for each recipient

This commit is contained in:
Ephraim Atta-Duncan
2023-08-29 18:23:52 +00:00
committed by Mythie
parent 1bce169228
commit ebcd7c78e4
12 changed files with 170 additions and 102 deletions

View File

@ -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(
(
<div
style={{
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: `url('data:image/png;base64,${Buffer.from(
imageData as unknown as string,
).toString('base64')}')`,
backgroundSize: '1200px 850px',
backgroundPositionY: '0%',
}}
>
<div tw="p-16 border-solid border-2 border-sky-500">
<div tw=" text-[#64748B99]">Duncan</div>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Caveat',
data: fontData,
style: 'italic',
},
],
},
);
}

View File

@ -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 (
<div>
<h1>Share Page</h1>
</div>
);
}

View File

@ -1,9 +0,0 @@
import React from 'react';
export default async function SharePage() {
return (
<div>
<h1>Share Page</h1>
</div>
);
}

View File

@ -1,16 +1,16 @@
import Link from 'next/link'; import Link from 'next/link';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { CheckCircle2, Clock8, Share } from 'lucide-react'; import { CheckCircle2, Clock8 } from 'lucide-react';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; 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 { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { DocumentStatus, FieldType } from '@documenso/prisma/client'; import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { DownloadButton } from './download-button'; import { DownloadButton } from './download-button';
import { ShareButton } from './share-button';
import { SigningCard } from './signing-card'; import { SigningCard } from './signing-card';
export type CompletedSigningPageProps = { export type CompletedSigningPageProps = {
@ -88,11 +88,7 @@ export default async function CompletedSigningPage({
))} ))}
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4"> <div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
{/* TODO: Hook this up */} <ShareButton documentId={document.id} recipientId={recipient.id} />
<Button variant="outline" className="flex-1">
<Share className="mr-2 h-5 w-5" />
Share
</Button>
<DownloadButton <DownloadButton
className="flex-1" className="flex-1"

View File

@ -0,0 +1,41 @@
'use client';
import { HTMLAttributes } from 'react';
import { useRouter } from 'next/navigation';
import { Share } from 'lucide-react';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
export type ShareButtonProps = HTMLAttributes<HTMLButtonElement> & {
recipientId: number;
documentId: number;
};
export const ShareButton = ({ recipientId, documentId }: ShareButtonProps) => {
const { mutateAsync: createShareId } = trpc.share.create.useMutation();
const router = useRouter();
return (
<Button
variant="outline"
className="flex-1"
onClick={async () => {
// redirect to the share page
// create link once and dont allow a user to create the link
const response = await createShareId({
recipientId,
documentId,
});
return router.push(`/share/${response.link}`);
}}
>
<Share className="mr-2 h-5 w-5" />
Share
</Button>
);
};

View File

@ -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;
};

View File

@ -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;

View File

@ -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;

View File

@ -102,32 +102,15 @@ enum DocumentStatus {
model Document { model Document {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
created DateTime @default(now())
userId Int userId Int
User User @relation(fields: [userId], references: [id], onDelete: Cascade) User User @relation(fields: [userId], references: [id], onDelete: Cascade)
title String title String
status DocumentStatus @default(DRAFT) status DocumentStatus @default(DRAFT)
document String
Recipient Recipient[] Recipient Recipient[]
Field Field[] Field Field[]
documentDataId String Share Share[]
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?
} }
enum ReadStatus { enum ReadStatus {
@ -159,6 +142,7 @@ model Recipient {
Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
Field Field[] Field Field[]
Signature Signature[] Signature Signature[]
Share Share[]
@@unique([documentId, email]) @@unique([documentId, email])
} }
@ -201,11 +185,13 @@ model Signature {
Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict) Field Field @relation(fields: [fieldId], references: [id], onDelete: Restrict)
} }
model PasswordResetToken { model Share {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
token String @unique recipientId Int
recipent Recipient @relation(fields: [recipientId], references: [id])
link String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
expiry DateTime updatedAt DateTime @updatedAt
userId Int document Document? @relation(fields: [documentId], references: [id])
User User @relation(fields: [userId], references: [id]) documentId Int?
} }

View File

@ -3,6 +3,7 @@ import { authRouter } from './auth-router/router';
import { documentRouter } from './document-router/router'; import { documentRouter } from './document-router/router';
import { fieldRouter } from './field-router/router'; import { fieldRouter } from './field-router/router';
import { profileRouter } from './profile-router/router'; import { profileRouter } from './profile-router/router';
import { shareRouter } from './share-router/router';
import { procedure, router } from './trpc'; import { procedure, router } from './trpc';
export const appRouter = router({ export const appRouter = router({
@ -11,7 +12,7 @@ export const appRouter = router({
profile: profileRouter, profile: profileRouter,
document: documentRouter, document: documentRouter,
field: fieldRouter, field: fieldRouter,
admin: adminRouter, share: shareRouter,
}); });
export type AppRouter = typeof appRouter; export type AppRouter = typeof appRouter;

View File

@ -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.',
});
}
}),
});

View File

@ -0,0 +1,8 @@
import { z } from 'zod';
export const ZShareLinkSchema = z.object({
documentId: z.number(),
recipientId: z.number(),
});
export type ZShareLinkSchema = z.infer<typeof ZShareLinkSchema>;