mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +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 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"
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
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