feat: team api tokens
This commit is contained in:
@@ -61,7 +61,7 @@ export const DeleteDocumentDialog = ({
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
await deleteDocument({ id, status });
|
||||
await deleteDocument({ id });
|
||||
} catch {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamTokens } from '@documenso/lib/server-only/public-api/get-all-team-tokens';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
import DeleteTokenDialog from '~/components/(dashboard)/settings/token/delete-token-dialog';
|
||||
import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { ApiTokenForm } from '~/components/forms/token';
|
||||
|
||||
type ApiTokensPageProps = {
|
||||
params: {
|
||||
teamUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
const team = await getTeamByUrl({ userId: user.id, teamUrl });
|
||||
|
||||
const tokens = await getTeamTokens({ userId: user.id, teamId: team.id });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">API Tokens</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
On this page, you can create new API tokens and manage the existing ones.
|
||||
</p>
|
||||
|
||||
<hr className="my-4" />
|
||||
|
||||
<ApiTokenForm className="max-w-xl" teamId={team.id} />
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
<h4 className="text-xl font-medium">Your existing tokens</h4>
|
||||
|
||||
{tokens.length === 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
Your tokens will be shown here once you create them.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tokens.length > 0 && (
|
||||
<div className="mt-4 flex max-w-xl flex-col gap-y-4">
|
||||
{tokens.map((token) => (
|
||||
<div key={token.id} className="border-border rounded-lg border p-4">
|
||||
<div className="flex items-center justify-between gap-x-4">
|
||||
<div>
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Created on <LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
Expires on <LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
Token doesn't have an expiration date
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DeleteTokenDialog token={token} teamId={team.id}>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
</DeleteTokenDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,12 +32,18 @@ import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type DeleteTokenDialogProps = {
|
||||
teamId?: number;
|
||||
token: Pick<ApiToken, 'id' | 'name'>;
|
||||
onDelete?: () => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function DeleteTokenDialog({ token, onDelete, children }: DeleteTokenDialogProps) {
|
||||
export default function DeleteTokenDialog({
|
||||
teamId,
|
||||
token,
|
||||
onDelete,
|
||||
children,
|
||||
}: DeleteTokenDialogProps) {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -70,6 +76,7 @@ export default function DeleteTokenDialog({ token, onDelete, children }: DeleteT
|
||||
try {
|
||||
await deleteTokenMutation({
|
||||
id: token.id,
|
||||
teamId,
|
||||
});
|
||||
|
||||
toast({
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
|
||||
import { CreditCard, Settings, Users } from 'lucide-react';
|
||||
import { Braces, CreditCard, Settings, Users } from 'lucide-react';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -21,6 +21,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
|
||||
const settingsPath = `/t/${teamUrl}/settings`;
|
||||
const membersPath = `/t/${teamUrl}/settings/members`;
|
||||
const tokensPath = `/t/${teamUrl}/settings/tokens`;
|
||||
const billingPath = `/t/${teamUrl}/settings/billing`;
|
||||
|
||||
return (
|
||||
@@ -48,6 +49,16 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={tokensPath}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith(tokensPath) && 'bg-secondary')}
|
||||
>
|
||||
<Braces className="mr-2 h-5 w-5" />
|
||||
API Tokens
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link href={billingPath}>
|
||||
<Button
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams, usePathname } from 'next/navigation';
|
||||
|
||||
import { CreditCard, Key, User } from 'lucide-react';
|
||||
import { Braces, CreditCard, Key, User } from 'lucide-react';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@@ -21,6 +21,7 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
|
||||
const settingsPath = `/t/${teamUrl}/settings`;
|
||||
const membersPath = `/t/${teamUrl}/settings/members`;
|
||||
const tokensPath = `/t/${teamUrl}/settings/tokens`;
|
||||
const billingPath = `/t/${teamUrl}/settings/billing`;
|
||||
|
||||
return (
|
||||
@@ -56,6 +57,16 @@ export const MobileNav = ({ className, ...props }: MobileNavProps) => {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Link href={tokensPath}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn('w-full justify-start', pathname?.startsWith(tokensPath) && 'bg-secondary')}
|
||||
>
|
||||
<Braces className="mr-2 h-5 w-5" />
|
||||
API Tokens
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
{IS_BILLING_ENABLED() && (
|
||||
<Link href={billingPath}>
|
||||
<Button
|
||||
|
||||
@@ -46,9 +46,10 @@ type TCreateTokenFormSchema = z.infer<typeof ZCreateTokenFormSchema>;
|
||||
|
||||
export type ApiTokenFormProps = {
|
||||
className?: string;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const ApiTokenForm = ({ className }: ApiTokenFormProps) => {
|
||||
export const ApiTokenForm = ({ className, teamId }: ApiTokenFormProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const [, copy] = useCopyToClipboard();
|
||||
@@ -96,6 +97,7 @@ export const ApiTokenForm = ({ className }: ApiTokenFormProps) => {
|
||||
const onSubmit = async ({ tokenName, expirationDate }: TCreateTokenMutationSchema) => {
|
||||
try {
|
||||
await createTokenMutation({
|
||||
teamId,
|
||||
tokenName,
|
||||
expirationDate: noExpirationDate ? null : expirationDate,
|
||||
});
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"next-runtime-env": "^3.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"next-auth": {
|
||||
"next": "14.0.3"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { initContract } from '@ts-rest/core';
|
||||
|
||||
import {
|
||||
ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema,
|
||||
ZAuthorizationHeadersSchema,
|
||||
ZCreateDocumentFromTemplateMutationResponseSchema,
|
||||
ZCreateDocumentFromTemplateMutationSchema,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
ZDeleteFieldMutationSchema,
|
||||
ZDeleteRecipientMutationSchema,
|
||||
ZGetDocumentsQuerySchema,
|
||||
ZSendDocumentForSigningMutationSchema,
|
||||
ZSuccessfulDocumentResponseSchema,
|
||||
ZSuccessfulFieldResponseSchema,
|
||||
ZSuccessfulGetDocumentResponseSchema,
|
||||
@@ -72,13 +72,13 @@ export const ApiContractV1 = c.router(
|
||||
401: ZUnsuccessfulResponseSchema,
|
||||
404: ZUnsuccessfulResponseSchema,
|
||||
},
|
||||
summary: 'Upload a new document and get a presigned URL',
|
||||
summary: 'Create a new document from an existing template',
|
||||
},
|
||||
|
||||
sendDocument: {
|
||||
method: 'POST',
|
||||
path: '/api/v1/documents/:id/send',
|
||||
body: SendDocumentMutationSchema,
|
||||
body: ZSendDocumentForSigningMutationSchema,
|
||||
responses: {
|
||||
200: ZSuccessfulSigningResponseSchema,
|
||||
400: ZUnsuccessfulResponseSchema,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g
|
||||
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 { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||
import { DocumentDataType, DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
@@ -25,11 +26,16 @@ import { ApiContractV1 } from './contract';
|
||||
import { authenticatedMiddleware } from './middleware/authenticated';
|
||||
|
||||
export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
getDocuments: authenticatedMiddleware(async (args, user) => {
|
||||
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
||||
const page = Number(args.query.page) || 1;
|
||||
const perPage = Number(args.query.perPage) || 10;
|
||||
|
||||
const { data: documents, totalPages } = await findDocuments({ page, perPage, userId: user.id });
|
||||
const { data: documents, totalPages } = await findDocuments({
|
||||
page,
|
||||
perPage,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
@@ -40,13 +46,19 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
getDocument: authenticatedMiddleware(async (args, user) => {
|
||||
getDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
|
||||
try {
|
||||
const document = await getDocumentById({ id: Number(documentId), userId: user.id });
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
teamId: team?.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
@@ -67,16 +79,29 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
deleteDocument: authenticatedMiddleware(async (args, user) => {
|
||||
deleteDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
|
||||
try {
|
||||
const document = await getDocumentById({ id: Number(documentId), userId: user.id });
|
||||
|
||||
const deletedDocument = await deleteDocument({
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
status: document.status,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const deletedDocument = await deleteDocument({
|
||||
id: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -93,7 +118,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createDocument: authenticatedMiddleware(async (args, user) => {
|
||||
createDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { body } = args;
|
||||
|
||||
try {
|
||||
@@ -118,13 +143,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const document = await createDocument({
|
||||
title: body.title,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
documentDataId: documentData.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
const recipients = await setRecipientsForDocument({
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
documentId: document.id,
|
||||
recipients: body.recipients,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -151,7 +180,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user) => {
|
||||
createDocumentFromTemplate: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { body, params } = args;
|
||||
|
||||
const templateId = Number(params.templateId);
|
||||
@@ -161,14 +190,16 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const document = await createDocumentFromTemplate({
|
||||
templateId,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
recipients: body.recipients,
|
||||
});
|
||||
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
data: {
|
||||
title: body.title,
|
||||
title: fileName,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -180,6 +211,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
message: body.meta.message,
|
||||
dateFormat: body.meta.dateFormat,
|
||||
timezone: body.meta.timezone,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,10 +230,10 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
sendDocument: authenticatedMiddleware(async (args, user) => {
|
||||
sendDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id } = args.params;
|
||||
|
||||
const document = await getDocumentById({ id: Number(id), userId: user.id });
|
||||
const document = await getDocumentById({ id: Number(id), userId: user.id, teamId: team?.id });
|
||||
|
||||
if (!document) {
|
||||
return {
|
||||
@@ -212,11 +244,11 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}
|
||||
|
||||
if (document.status === 'PENDING') {
|
||||
if (document.status === DocumentStatus.COMPLETED) {
|
||||
return {
|
||||
status: 400,
|
||||
body: {
|
||||
message: 'Document is already waiting for signing',
|
||||
message: 'Document is already complete',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -258,6 +290,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
await sendDocument({
|
||||
documentId: Number(id),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -276,13 +310,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
createRecipient: authenticatedMiddleware(async (args, user) => {
|
||||
createRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { name, email, role } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
@@ -306,6 +341,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
const recipientAlreadyExists = recipients.some((recipient) => recipient.email === email);
|
||||
@@ -323,6 +359,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const newRecipients = await setRecipientsForDocument({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
recipients: [
|
||||
...recipients,
|
||||
{
|
||||
@@ -331,6 +368,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
role,
|
||||
},
|
||||
],
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
||||
@@ -356,13 +394,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
}
|
||||
}),
|
||||
|
||||
updateRecipient: authenticatedMiddleware(async (args, user) => {
|
||||
updateRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, recipientId } = args.params;
|
||||
const { name, email, role } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
@@ -386,9 +425,12 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const updatedRecipient = await updateRecipient({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!updatedRecipient) {
|
||||
@@ -409,12 +451,13 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
deleteRecipient: authenticatedMiddleware(async (args, user) => {
|
||||
deleteRecipient: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, recipientId } = args.params;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
@@ -438,6 +481,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const deletedRecipient = await deleteRecipient({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!deletedRecipient) {
|
||||
@@ -458,13 +504,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
createField: authenticatedMiddleware(async (args, user) => {
|
||||
createField: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId } = args.params;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
@@ -511,12 +558,15 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const field = await createField({
|
||||
documentId: Number(documentId),
|
||||
recipientId: Number(recipientId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
type,
|
||||
pageNumber,
|
||||
pageX,
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
const remappedField = {
|
||||
@@ -542,13 +592,14 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
updateField: authenticatedMiddleware(async (args, user) => {
|
||||
updateField: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, fieldId } = args.params;
|
||||
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
|
||||
|
||||
const document = await getDocumentById({
|
||||
id: Number(documentId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
@@ -594,6 +645,8 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
|
||||
const updatedField = await updateField({
|
||||
fieldId: Number(fieldId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
documentId: Number(documentId),
|
||||
recipientId: recipientId ? Number(recipientId) : undefined,
|
||||
type,
|
||||
@@ -602,6 +655,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
});
|
||||
|
||||
const remappedField = {
|
||||
@@ -627,7 +681,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
};
|
||||
}),
|
||||
|
||||
deleteField: authenticatedMiddleware(async (args, user) => {
|
||||
deleteField: authenticatedMiddleware(async (args, user, team) => {
|
||||
const { id: documentId, fieldId } = args.params;
|
||||
|
||||
const document = await getDocumentById({
|
||||
@@ -684,6 +738,9 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
|
||||
const deletedField = await deleteField({
|
||||
documentId: Number(documentId),
|
||||
fieldId: Number(fieldId),
|
||||
userId: user.id,
|
||||
teamId: team?.id,
|
||||
requestMetadata: extractNextApiRequestMetadata(args.req),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!deletedField) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
|
||||
import { getUserByApiToken } from '@documenso/lib/server-only/public-api/get-user-by-token';
|
||||
import type { User } from '@documenso/prisma/client';
|
||||
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||
import type { Team, User } from '@documenso/prisma/client';
|
||||
|
||||
export const authenticatedMiddleware = <
|
||||
T extends {
|
||||
@@ -12,7 +12,7 @@ export const authenticatedMiddleware = <
|
||||
body: unknown;
|
||||
},
|
||||
>(
|
||||
handler: (args: T, user: User) => Promise<R>,
|
||||
handler: (args: T, user: User, team?: Team | null) => Promise<R>,
|
||||
) => {
|
||||
return async (args: T) => {
|
||||
try {
|
||||
@@ -25,9 +25,9 @@ export const authenticatedMiddleware = <
|
||||
throw new Error('Token was not provided for authenticated middleware');
|
||||
}
|
||||
|
||||
const user = await getUserByApiToken({ token });
|
||||
const apiToken = await getApiTokenByToken({ token });
|
||||
|
||||
return await handler(args, user);
|
||||
return await handler(args, apiToken.user, apiToken.team);
|
||||
} catch (_err) {
|
||||
console.log({ _err });
|
||||
return {
|
||||
|
||||
@@ -25,6 +25,7 @@ export type TDeleteDocumentMutationSchema = typeof ZDeleteDocumentMutationSchema
|
||||
export const ZSuccessfulDocumentResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
userId: z.number(),
|
||||
teamId: z.number().nullish(),
|
||||
title: z.string(),
|
||||
status: z.string(),
|
||||
documentDataId: z.string(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Prisma } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
export interface FindDocumentsOptions {
|
||||
term?: string;
|
||||
|
||||
@@ -14,39 +14,45 @@ import { FROM_ADDRESS, FROM_NAME } from '../../constants/email';
|
||||
export type DeleteDocumentOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
status: DocumentStatus;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const deleteDocument = async ({ id, userId, status }: DeleteDocumentOptions) => {
|
||||
export const deleteDocument = async ({ id, userId, teamId }: DeleteDocumentOptions) => {
|
||||
// if the document is a draft, hard-delete
|
||||
if (status === DocumentStatus.DRAFT) {
|
||||
return await prisma.document.delete({ where: { id, userId, status: DocumentStatus.DRAFT } });
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
User: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const { status, User: user } = document;
|
||||
|
||||
// if the document is pending, send cancellation emails to all recipients
|
||||
if (status === DocumentStatus.PENDING) {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id,
|
||||
status,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
documentMeta: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
if (document.Recipient.length > 0) {
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
@@ -81,6 +87,7 @@ export const deleteDocument = async ({ id, userId, status }: DeleteDocumentOptio
|
||||
return await prisma.document.update({
|
||||
where: {
|
||||
id,
|
||||
teamId,
|
||||
},
|
||||
data: {
|
||||
deletedAt: new Date().toISOString(),
|
||||
|
||||
@@ -16,9 +16,8 @@ import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { Prisma } from '@documenso/prisma/client';
|
||||
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { getDocumentWhereInput } from './get-document-by-id';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
documentId: number;
|
||||
|
||||
@@ -20,12 +20,14 @@ import {
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const sendDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: SendDocumentOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
@@ -42,20 +44,21 @@ export const sendDocument = async ({
|
||||
const document = await prisma.document.findUnique({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
|
||||
@@ -5,16 +5,36 @@ import type { Prisma } from '@prisma/client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type UpdateDocumentOptions = {
|
||||
documentId: number;
|
||||
data: Prisma.DocumentUpdateInput;
|
||||
userId: number;
|
||||
documentId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const updateDocument = async ({ documentId, userId, data }: UpdateDocumentOptions) => {
|
||||
export const updateDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
data,
|
||||
}: UpdateDocumentOptions) => {
|
||||
return await prisma.document.update({
|
||||
where: {
|
||||
id: documentId,
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
data: {
|
||||
...data,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { prisma } from '@documenso/prisma';
|
||||
|
||||
export type UpdateTitleOptions = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
title: string;
|
||||
requestMetadata?: RequestMetadata;
|
||||
@@ -14,6 +15,7 @@ export type UpdateTitleOptions = {
|
||||
|
||||
export const updateTitle = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
title,
|
||||
requestMetadata,
|
||||
@@ -28,20 +30,21 @@ export const updateTitle = async ({
|
||||
const document = await tx.document.findFirstOrThrow({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
import type { FieldType, Team } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type CreateFieldOptions = {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
recipientId: number;
|
||||
type: FieldType;
|
||||
pageNumber: number;
|
||||
@@ -10,10 +15,13 @@ export type CreateFieldOptions = {
|
||||
pageY: number;
|
||||
pageWidth: number;
|
||||
pageHeight: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const createField = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
@@ -21,7 +29,62 @@ export const createField = async ({
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata,
|
||||
}: CreateFieldOptions) => {
|
||||
const document = await prisma.document.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
where: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!document) {
|
||||
throw new Error('Document not found');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const field = await prisma.field.create({
|
||||
data: {
|
||||
documentId,
|
||||
@@ -35,6 +98,28 @@ export const createField = async ({
|
||||
customText: '',
|
||||
inserted: false,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_CREATED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
|
||||
@@ -1,16 +1,89 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type DeleteFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteField = async ({ fieldId, documentId }: DeleteFieldOptions) => {
|
||||
export const deleteField = async ({
|
||||
fieldId,
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
}: DeleteFieldOptions) => {
|
||||
const field = await prisma.field.delete({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
Document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_DELETED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientId: field.recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { FieldType } from '@documenso/prisma/client';
|
||||
import type { FieldType, Team } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type UpdateFieldOptions = {
|
||||
fieldId: number;
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
recipientId?: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
@@ -11,11 +16,14 @@ export type UpdateFieldOptions = {
|
||||
pageY?: number;
|
||||
pageWidth?: number;
|
||||
pageHeight?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updateField = async ({
|
||||
fieldId,
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
type,
|
||||
pageNumber,
|
||||
@@ -23,11 +31,29 @@ export const updateField = async ({
|
||||
pageY,
|
||||
pageWidth,
|
||||
pageHeight,
|
||||
requestMetadata,
|
||||
}: UpdateFieldOptions) => {
|
||||
const field = await prisma.field.update({
|
||||
where: {
|
||||
id: fieldId,
|
||||
documentId,
|
||||
Document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
recipientId,
|
||||
@@ -38,6 +64,58 @@ export const updateField = async ({
|
||||
width: pageWidth,
|
||||
height: pageHeight,
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!field) {
|
||||
throw new Error('Field not found');
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'FIELD_UPDATED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
fieldRecipientEmail: field.Recipient?.email ?? '',
|
||||
fieldRecipientId: recipientId ?? -1,
|
||||
fieldType: field.type,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return field;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Duration } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
// temporary choice for testing only
|
||||
import * as timeConstants from '../../constants/time';
|
||||
@@ -14,14 +15,16 @@ type TimeConstants = typeof timeConstants & {
|
||||
|
||||
type CreateApiTokenInput = {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
tokenName: string;
|
||||
expirationDate: string | null;
|
||||
expiresIn: string | null;
|
||||
};
|
||||
|
||||
export const createApiToken = async ({
|
||||
userId,
|
||||
teamId,
|
||||
tokenName,
|
||||
expirationDate,
|
||||
expiresIn,
|
||||
}: CreateApiTokenInput) => {
|
||||
const apiToken = `api_${alphaid(16)}`;
|
||||
|
||||
@@ -29,23 +32,36 @@ export const createApiToken = async ({
|
||||
|
||||
const timeConstantsRecords: TimeConstants = timeConstants;
|
||||
|
||||
const dbToken = await prisma.apiToken.create({
|
||||
if (teamId) {
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new Error('You do not have permission to create a token for this team');
|
||||
}
|
||||
}
|
||||
|
||||
const storedToken = await prisma.apiToken.create({
|
||||
data: {
|
||||
token: hashedToken,
|
||||
name: tokenName,
|
||||
userId,
|
||||
expires: expirationDate
|
||||
? DateTime.now().plus(timeConstantsRecords[expirationDate]).toJSDate()
|
||||
: null,
|
||||
token: hashedToken,
|
||||
expires: expiresIn ? DateTime.now().plus(timeConstantsRecords[expiresIn]).toJSDate() : null,
|
||||
userId: teamId ? null : userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbToken) {
|
||||
if (!storedToken) {
|
||||
throw new Error('Failed to create the API token');
|
||||
}
|
||||
|
||||
return {
|
||||
id: dbToken.id,
|
||||
id: storedToken.id,
|
||||
token: apiToken,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export type DeleteTokenByIdOptions = {
|
||||
id: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
};
|
||||
|
||||
export const deleteTokenById = async ({ id, userId }: DeleteTokenByIdOptions) => {
|
||||
export const deleteTokenById = async ({ id, userId, teamId }: DeleteTokenByIdOptions) => {
|
||||
if (teamId) {
|
||||
const member = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
role: TeamMemberRole.ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new Error('You do not have permission to delete this token');
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.apiToken.delete({
|
||||
where: {
|
||||
id,
|
||||
userId,
|
||||
userId: teamId ? null : userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
36
packages/lib/server-only/public-api/get-all-team-tokens.ts
Normal file
36
packages/lib/server-only/public-api/get-all-team-tokens.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { TeamMemberRole } from '@documenso/prisma/client';
|
||||
|
||||
export type GetUserTokensOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
};
|
||||
|
||||
export const getTeamTokens = async ({ userId, teamId }: GetUserTokensOptions) => {
|
||||
const teamMember = await prisma.teamMember.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
if (teamMember?.role !== TeamMemberRole.ADMIN) {
|
||||
throw new Error('You do not have permission to view tokens for this team');
|
||||
}
|
||||
|
||||
return await prisma.apiToken.findMany({
|
||||
where: {
|
||||
teamId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
algorithm: true,
|
||||
createdAt: true,
|
||||
expires: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
export const getApiTokenByToken = async ({ token }: { token: string }) => {
|
||||
const hashedToken = hashString(token);
|
||||
|
||||
const apiToken = await prisma.apiToken.findFirst({
|
||||
where: {
|
||||
token: hashedToken,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!apiToken) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
if (apiToken.expires && apiToken.expires < new Date()) {
|
||||
throw new Error('Expired token');
|
||||
}
|
||||
|
||||
if (apiToken.team) {
|
||||
apiToken.user = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: apiToken.team.ownerUserId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { user } = apiToken;
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
return { ...apiToken, user };
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { hashString } from '../auth/hash';
|
||||
|
||||
export const getUserByApiToken = async ({ token }: { token: string }) => {
|
||||
const hashedToken = hashString(token);
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
ApiToken: {
|
||||
some: {
|
||||
token: hashedToken,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
ApiToken: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
const retrievedToken = user.ApiToken.find((apiToken) => apiToken.token === hashedToken);
|
||||
|
||||
// This should be impossible but we need to satisfy TypeScript
|
||||
if (!retrievedToken) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
if (retrievedToken.expires && retrievedToken.expires < new Date()) {
|
||||
throw new Error('Expired token');
|
||||
}
|
||||
|
||||
return user;
|
||||
};
|
||||
@@ -1,16 +1,46 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Team } from '@documenso/prisma/client';
|
||||
import { SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
|
||||
export type DeleteRecipientOptions = {
|
||||
documentId: number;
|
||||
recipientId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const deleteRecipient = async ({ documentId, recipientId }: DeleteRecipientOptions) => {
|
||||
export const deleteRecipient = async ({
|
||||
documentId,
|
||||
recipientId,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: DeleteRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
documentId,
|
||||
Document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,11 +52,55 @@ export const deleteRecipient = async ({ documentId, recipientId }: DeleteRecipie
|
||||
throw new Error('Can not delete a recipient that has already been sent a document');
|
||||
}
|
||||
|
||||
const deletedRecipient = await prisma.recipient.delete({
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
const deleted = await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: 'RECIPIENT_DELETED',
|
||||
documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
email: team?.name ?? user.email,
|
||||
name: team ? '' : user.name,
|
||||
},
|
||||
data: {
|
||||
recipientEmail: recipient.email,
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
},
|
||||
requestMetadata,
|
||||
}),
|
||||
});
|
||||
|
||||
return deleted;
|
||||
});
|
||||
|
||||
return deletedRecipient;
|
||||
};
|
||||
|
||||
@@ -3,11 +3,13 @@ import { prisma } from '@documenso/prisma';
|
||||
export interface GetRecipientsForDocumentOptions {
|
||||
documentId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
export const getRecipientsForDocument = async ({
|
||||
documentId,
|
||||
userId,
|
||||
teamId,
|
||||
}: GetRecipientsForDocumentOptions) => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
@@ -18,6 +20,7 @@ export const getRecipientsForDocument = async ({
|
||||
userId,
|
||||
},
|
||||
{
|
||||
teamId,
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export interface SetRecipientsForDocumentOptions {
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
documentId: number;
|
||||
recipients: {
|
||||
id?: number | null;
|
||||
@@ -23,6 +24,7 @@ export interface SetRecipientsForDocumentOptions {
|
||||
|
||||
export const setRecipientsForDocument = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
recipients,
|
||||
requestMetadata,
|
||||
@@ -30,20 +32,21 @@ export const setRecipientsForDocument = async ({
|
||||
const document = await prisma.document.findFirst({
|
||||
where: {
|
||||
id: documentId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,7 +109,7 @@ export const setRecipientsForDocument = async ({
|
||||
});
|
||||
|
||||
const persistedRecipients = await prisma.$transaction(async (tx) => {
|
||||
await Promise.all(
|
||||
return await Promise.all(
|
||||
linkedRecipients.map(async (recipient) => {
|
||||
const upsertedRecipient = await tx.recipient.upsert({
|
||||
where: {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { RecipientRole } from '@documenso/prisma/client';
|
||||
import type { RecipientRole, Team } from '@documenso/prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData, diffRecipientChanges } from '../../utils/document-audit-logs';
|
||||
|
||||
export type UpdateRecipientOptions = {
|
||||
documentId: number;
|
||||
@@ -7,6 +11,9 @@ export type UpdateRecipientOptions = {
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
requestMetadata?: RequestMetadata;
|
||||
};
|
||||
|
||||
export const updateRecipient = async ({
|
||||
@@ -15,11 +22,52 @@ export const updateRecipient = async ({
|
||||
email,
|
||||
name,
|
||||
role,
|
||||
userId,
|
||||
teamId,
|
||||
requestMetadata,
|
||||
}: UpdateRecipientOptions) => {
|
||||
const recipient = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
documentId,
|
||||
Document: {
|
||||
id: documentId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let team: Team | null = null;
|
||||
|
||||
if (teamId) {
|
||||
team = await prisma.team.findFirst({
|
||||
where: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -27,15 +75,43 @@ export const updateRecipient = async ({
|
||||
throw new Error('Recipient not found');
|
||||
}
|
||||
|
||||
const updatedRecipient = await prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
email: email?.toLowerCase() ?? recipient.email,
|
||||
name: name ?? recipient.name,
|
||||
role: role ?? recipient.role,
|
||||
},
|
||||
const updatedRecipient = await prisma.$transaction(async (tx) => {
|
||||
const persisted = await prisma.recipient.update({
|
||||
where: {
|
||||
id: recipient.id,
|
||||
},
|
||||
data: {
|
||||
email: email?.toLowerCase() ?? recipient.email,
|
||||
name: name ?? recipient.name,
|
||||
role: role ?? recipient.role,
|
||||
},
|
||||
});
|
||||
|
||||
const changes = diffRecipientChanges(recipient, persisted);
|
||||
|
||||
if (changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: documentId,
|
||||
user: {
|
||||
id: team?.id ?? user.id,
|
||||
name: team?.name ?? user.name,
|
||||
email: team ? '' : user.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
changes,
|
||||
recipientId,
|
||||
recipientEmail: persisted.email,
|
||||
recipientName: persisted.name,
|
||||
recipientRole: persisted.role,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return persisted;
|
||||
}
|
||||
});
|
||||
|
||||
return updatedRecipient;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { RecipientRole } from '@documenso/prisma/client';
|
||||
export type CreateDocumentFromTemplateOptions = {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
teamId?: number;
|
||||
recipients?: {
|
||||
name?: string;
|
||||
email: string;
|
||||
@@ -15,25 +16,27 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
export const createDocumentFromTemplate = async ({
|
||||
templateId,
|
||||
userId,
|
||||
teamId,
|
||||
recipients,
|
||||
}: CreateDocumentFromTemplateOptions) => {
|
||||
const template = await prisma.template.findUnique({
|
||||
where: {
|
||||
id: templateId,
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
...(teamId
|
||||
? {
|
||||
team: {
|
||||
id: teamId,
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
userId,
|
||||
teamId: null,
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
Recipient: true,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ApiToken" ADD COLUMN "teamId" INTEGER,
|
||||
ALTER COLUMN "userId" DROP NOT NULL;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiToken" ADD CONSTRAINT "ApiToken_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -19,19 +19,19 @@ enum Role {
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
id Int @id @default(autoincrement())
|
||||
name String?
|
||||
customerId String? @unique
|
||||
email String @unique
|
||||
customerId String? @unique
|
||||
email String @unique
|
||||
emailVerified DateTime?
|
||||
password String?
|
||||
source String?
|
||||
signature String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
lastSignedIn DateTime @default(now())
|
||||
roles Role[] @default([USER])
|
||||
identityProvider IdentityProvider @default(DOCUMENSO)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
lastSignedIn DateTime @default(now())
|
||||
roles Role[] @default([USER])
|
||||
identityProvider IdentityProvider @default(DOCUMENSO)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
Document Document[]
|
||||
@@ -41,12 +41,12 @@ model User {
|
||||
ownedPendingTeams TeamPending[]
|
||||
teamMembers TeamMember[]
|
||||
twoFactorSecret String?
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorBackupCodes String?
|
||||
VerificationToken VerificationToken[]
|
||||
ApiToken ApiToken[]
|
||||
Template Template[]
|
||||
securityAuditLogs UserSecurityAuditLog[]
|
||||
securityAuditLogs UserSecurityAuditLog[]
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
@@ -105,8 +105,10 @@ model ApiToken {
|
||||
algorithm ApiTokenAlgorithm @default(SHA512)
|
||||
expires DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId Int?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
enum SubscriptionStatus {
|
||||
@@ -225,14 +227,14 @@ model DocumentData {
|
||||
}
|
||||
|
||||
model DocumentMeta {
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
documentId Int @unique
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
documentId Int @unique
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
redirectUrl String?
|
||||
}
|
||||
|
||||
@@ -372,6 +374,7 @@ model Team {
|
||||
|
||||
document Document[]
|
||||
templates Template[]
|
||||
ApiToken ApiToken[]
|
||||
}
|
||||
|
||||
model TeamPending {
|
||||
|
||||
@@ -46,12 +46,13 @@ export const apiTokenRouter = router({
|
||||
.input(ZCreateTokenMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { tokenName, expirationDate } = input;
|
||||
const { tokenName, teamId, expirationDate } = input;
|
||||
|
||||
return await createApiToken({
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
tokenName,
|
||||
expirationDate,
|
||||
expiresIn: expirationDate,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new TRPCError({
|
||||
@@ -65,10 +66,11 @@ export const apiTokenRouter = router({
|
||||
.input(ZDeleteTokenByIdMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id } = input;
|
||||
const { id, teamId } = input;
|
||||
|
||||
return await deleteTokenById({
|
||||
id,
|
||||
teamId,
|
||||
userId: ctx.user.id,
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -7,6 +7,7 @@ export const ZGetApiTokenByIdQuerySchema = z.object({
|
||||
export type TGetApiTokenByIdQuerySchema = z.infer<typeof ZGetApiTokenByIdQuerySchema>;
|
||||
|
||||
export const ZCreateTokenMutationSchema = z.object({
|
||||
teamId: z.number().optional(),
|
||||
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),
|
||||
expirationDate: z.string().nullable(),
|
||||
});
|
||||
@@ -15,6 +16,7 @@ export type TCreateTokenMutationSchema = z.infer<typeof ZCreateTokenMutationSche
|
||||
|
||||
export const ZDeleteTokenByIdMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
teamId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TDeleteTokenByIdMutationSchema = z.infer<typeof ZDeleteTokenByIdMutationSchema>;
|
||||
|
||||
@@ -107,11 +107,11 @@ export const documentRouter = router({
|
||||
.input(ZDeleteDraftDocumentMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
try {
|
||||
const { id, status } = input;
|
||||
const { id } = input;
|
||||
|
||||
const userId = ctx.user.id;
|
||||
|
||||
return await deleteDocument({ id, userId, status });
|
||||
return await deleteDocument({ id, userId });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { URL_REGEX } from '@documenso/lib/constants/url-regex';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||
id: z.number().min(1),
|
||||
@@ -102,7 +102,6 @@ export type TSendDocumentMutationSchema = z.infer<typeof ZSendDocumentMutationSc
|
||||
|
||||
export const ZDeleteDraftDocumentMutationSchema = z.object({
|
||||
id: z.number().min(1),
|
||||
status: z.nativeEnum(DocumentStatus),
|
||||
});
|
||||
|
||||
export type TDeleteDraftDocumentMutationSchema = z.infer<typeof ZDeleteDraftDocumentMutationSchema>;
|
||||
|
||||
Reference in New Issue
Block a user