mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: create sharing id for each recipient
This commit is contained in:
committed by
Mythie
parent
1bce169228
commit
ebcd7c78e4
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
17
apps/web/src/app/(share)/share/[shortId]/page.tsx
Normal file
17
apps/web/src/app/(share)/share/[shortId]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default async function SharePage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Share Page</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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({
|
||||
))}
|
||||
|
||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||
{/* TODO: Hook this up */}
|
||||
<Button variant="outline" className="flex-1">
|
||||
<Share className="mr-2 h-5 w-5" />
|
||||
Share
|
||||
</Button>
|
||||
<ShareButton documentId={document.id} recipientId={recipient.id} />
|
||||
|
||||
<DownloadButton
|
||||
className="flex-1"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
20
packages/lib/server-only/share/create-share-id.ts
Normal file
20
packages/lib/server-only/share/create-share-id.ts
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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?
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
23
packages/trpc/server/share-router/router.ts
Normal file
23
packages/trpc/server/share-router/router.ts
Normal 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.',
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
8
packages/trpc/server/share-router/schema.ts
Normal file
8
packages/trpc/server/share-router/schema.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user