;
@@ -137,6 +138,10 @@ export default function UserPage({ params }: { params: { id: number } }) {
+
+
+
+ {user && }
);
}
diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx
index 577e0739a..1a5d2f554 100644
--- a/apps/web/src/app/(dashboard)/admin/users/page.tsx
+++ b/apps/web/src/app/(dashboard)/admin/users/page.tsx
@@ -19,7 +19,7 @@ export default async function AdminManageUsers({ searchParams = {} }: AdminManag
const [{ users, totalPages }, individualPrices] = await Promise.all([
search(searchString, page, perPage),
- getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY),
+ getPricesByPlan(STRIPE_PLAN_TYPE.COMMUNITY).catch(() => []),
]);
const individualPriceIds = individualPrices.map((price) => price.id);
diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
index 9eef1f4bd..262e297d6 100644
--- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
+++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx
@@ -57,7 +57,7 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
key={href}
href={`${rootHref}${href}`}
className={cn(
- 'text-muted-foreground dark:text-muted focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
+ 'text-muted-foreground dark:text-muted-foreground/60 focus-visible:ring-ring ring-offset-background rounded-md font-medium leading-5 hover:opacity-80 focus-visible:outline-none focus-visible:ring-2',
{
'text-foreground dark:text-muted-foreground': pathname?.startsWith(
`${rootHref}${href}`,
diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx
index 65bb63230..b0ede5b8b 100644
--- a/apps/web/src/components/(dashboard)/layout/header.tsx
+++ b/apps/web/src/components/(dashboard)/layout/header.tsx
@@ -86,7 +86,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
>
diff --git a/packages/lib/client-only/hooks/use-copy-share-link.ts b/packages/lib/client-only/hooks/use-copy-share-link.ts
index 255949e3c..cff552e8f 100644
--- a/packages/lib/client-only/hooks/use-copy-share-link.ts
+++ b/packages/lib/client-only/hooks/use-copy-share-link.ts
@@ -1,5 +1,5 @@
import { trpc } from '@documenso/trpc/react';
-import { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema';
+import type { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema';
import { useCopyToClipboard } from './use-copy-to-clipboard';
diff --git a/packages/lib/server-only/admin/get-entire-document.ts b/packages/lib/server-only/admin/get-entire-document.ts
new file mode 100644
index 000000000..e74ee4c7b
--- /dev/null
+++ b/packages/lib/server-only/admin/get-entire-document.ts
@@ -0,0 +1,26 @@
+import { prisma } from '@documenso/prisma';
+
+export type GetEntireDocumentOptions = {
+ id: number;
+};
+
+export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => {
+ const document = await prisma.document.findFirstOrThrow({
+ where: {
+ id,
+ },
+ include: {
+ Recipient: {
+ include: {
+ Field: {
+ include: {
+ Signature: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return document;
+};
diff --git a/packages/lib/server-only/admin/update-recipient.ts b/packages/lib/server-only/admin/update-recipient.ts
new file mode 100644
index 000000000..dcd826476
--- /dev/null
+++ b/packages/lib/server-only/admin/update-recipient.ts
@@ -0,0 +1,30 @@
+import { prisma } from '@documenso/prisma';
+import { SigningStatus } from '@documenso/prisma/client';
+
+export type UpdateRecipientOptions = {
+ id: number;
+ name: string | undefined;
+ email: string | undefined;
+};
+
+export const updateRecipient = async ({ id, name, email }: UpdateRecipientOptions) => {
+ const recipient = await prisma.recipient.findFirstOrThrow({
+ where: {
+ id,
+ },
+ });
+
+ if (recipient.signingStatus === SigningStatus.SIGNED) {
+ throw new Error('Cannot update a recipient that has already signed.');
+ }
+
+ return await prisma.recipient.update({
+ where: {
+ id,
+ },
+ data: {
+ name,
+ email,
+ },
+ });
+};
diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts
index 8f39e3d25..dd427dc95 100644
--- a/packages/lib/server-only/document/seal-document.ts
+++ b/packages/lib/server-only/document/seal-document.ts
@@ -22,12 +22,14 @@ import { sendCompletedEmail } from './send-completed-email';
export type SealDocumentOptions = {
documentId: number;
sendEmail?: boolean;
+ isResealing?: boolean;
requestMetadata?: RequestMetadata;
};
export const sealDocument = async ({
documentId,
sendEmail = true,
+ isResealing = false,
requestMetadata,
}: SealDocumentOptions) => {
'use server';
@@ -78,11 +80,20 @@ export const sealDocument = async ({
throw new Error(`Document ${document.id} has unsigned fields`);
}
+ if (isResealing) {
+ // If we're resealing we want to use the initial data for the document
+ // so we aren't placing fields on top of eachother.
+ documentData.data = documentData.initialData;
+ }
+
// !: Need to write the fields onto the document as a hard copy
const pdfData = await getFile(documentData);
const doc = await PDFDocument.load(pdfData);
+ // Flatten the form to stop annotation layers from appearing above documenso fields
+ doc.getForm().flatten();
+
for (const field of fields) {
await insertFieldInPDF(doc, field);
}
@@ -134,7 +145,7 @@ export const sealDocument = async ({
});
});
- if (sendEmail) {
+ if (sendEmail && !isResealing) {
await sendCompletedEmail({ documentId, requestMetadata });
}
diff --git a/packages/lib/server-only/user/delete-user.ts b/packages/lib/server-only/user/delete-user.ts
index d6d4284b4..71f579c26 100644
--- a/packages/lib/server-only/user/delete-user.ts
+++ b/packages/lib/server-only/user/delete-user.ts
@@ -4,20 +4,18 @@ import { DocumentStatus } from '@documenso/prisma/client';
import { deletedAccountServiceAccount } from './service-accounts/deleted-account';
export type DeleteUserOptions = {
- email: string;
+ id: number;
};
-export const deleteUser = async ({ email }: DeleteUserOptions) => {
+export const deleteUser = async ({ id }: DeleteUserOptions) => {
const user = await prisma.user.findFirst({
where: {
- email: {
- contains: email,
- },
+ id,
},
});
if (!user) {
- throw new Error(`User with email ${email} not found`);
+ throw new Error(`User with ID ${id} not found`);
}
const serviceAccount = await deletedAccountServiceAccount();
diff --git a/packages/trpc/server/admin-router/router.ts b/packages/trpc/server/admin-router/router.ts
index 7d71ab346..5be3ad9db 100644
--- a/packages/trpc/server/admin-router/router.ts
+++ b/packages/trpc/server/admin-router/router.ts
@@ -1,14 +1,39 @@
import { TRPCError } from '@trpc/server';
+import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents';
+import { updateRecipient } from '@documenso/lib/server-only/admin/update-recipient';
import { updateUser } from '@documenso/lib/server-only/admin/update-user';
+import { sealDocument } from '@documenso/lib/server-only/document/seal-document';
import { upsertSiteSetting } from '@documenso/lib/server-only/site-settings/upsert-site-setting';
+import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
+import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
import { adminProcedure, router } from '../trpc';
-import { ZUpdateProfileMutationByAdminSchema, ZUpdateSiteSettingMutationSchema } from './schema';
+import {
+ ZAdminDeleteUserMutationSchema,
+ ZAdminFindDocumentsQuerySchema,
+ ZAdminResealDocumentMutationSchema,
+ ZAdminUpdateProfileMutationSchema,
+ ZAdminUpdateRecipientMutationSchema,
+ ZAdminUpdateSiteSettingMutationSchema,
+} from './schema';
export const adminRouter = router({
+ findDocuments: adminProcedure.input(ZAdminFindDocumentsQuerySchema).query(async ({ input }) => {
+ const { term, page, perPage } = input;
+
+ try {
+ return await findDocuments({ term, page, perPage });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to retrieve the documents. Please try again.',
+ });
+ }
+ }),
+
updateUser: adminProcedure
- .input(ZUpdateProfileMutationByAdminSchema)
+ .input(ZAdminUpdateProfileMutationSchema)
.mutation(async ({ input }) => {
const { id, name, email, roles } = input;
@@ -22,8 +47,23 @@ export const adminRouter = router({
}
}),
+ updateRecipient: adminProcedure
+ .input(ZAdminUpdateRecipientMutationSchema)
+ .mutation(async ({ input }) => {
+ const { id, name, email } = input;
+
+ try {
+ return await updateRecipient({ id, name, email });
+ } catch (err) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to update the recipient provided.',
+ });
+ }
+ }),
+
updateSiteSetting: adminProcedure
- .input(ZUpdateSiteSettingMutationSchema)
+ .input(ZAdminUpdateSiteSettingMutationSchema)
.mutation(async ({ ctx, input }) => {
try {
const { id, enabled, data } = input;
@@ -41,4 +81,41 @@ export const adminRouter = router({
});
}
}),
+
+ resealDocument: adminProcedure
+ .input(ZAdminResealDocumentMutationSchema)
+ .mutation(async ({ input }) => {
+ const { id } = input;
+
+ try {
+ return await sealDocument({ documentId: id, isResealing: true });
+ } catch (err) {
+ console.log('resealDocument error', err);
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to reseal the document provided.',
+ });
+ }
+ }),
+
+ deleteUser: adminProcedure.input(ZAdminDeleteUserMutationSchema).mutation(async ({ input }) => {
+ const { id, email } = input;
+
+ try {
+ const user = await getUserById({ id });
+
+ if (user.email !== email) {
+ throw new Error('Email does not match');
+ }
+
+ return await deleteUser({ id });
+ } catch (err) {
+ console.log(err);
+
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'We were unable to delete the specified account. Please try again.',
+ });
+ }
+ }),
});
diff --git a/packages/trpc/server/admin-router/schema.ts b/packages/trpc/server/admin-router/schema.ts
index 0b99c8372..cfedb06ba 100644
--- a/packages/trpc/server/admin-router/schema.ts
+++ b/packages/trpc/server/admin-router/schema.ts
@@ -3,17 +3,48 @@ import z from 'zod';
import { ZSiteSettingSchema } from '@documenso/lib/server-only/site-settings/schema';
-export const ZUpdateProfileMutationByAdminSchema = z.object({
+export const ZAdminFindDocumentsQuerySchema = z.object({
+ term: z.string().optional(),
+ page: z.number().optional().default(1),
+ perPage: z.number().optional().default(20),
+});
+
+export type TAdminFindDocumentsQuerySchema = z.infer;
+
+export const ZAdminUpdateProfileMutationSchema = z.object({
id: z.number().min(1),
name: z.string().nullish(),
email: z.string().email().optional(),
roles: z.array(z.nativeEnum(Role)).optional(),
});
-export type TUpdateProfileMutationByAdminSchema = z.infer<
- typeof ZUpdateProfileMutationByAdminSchema
+export type TAdminUpdateProfileMutationSchema = z.infer;
+
+export const ZAdminUpdateRecipientMutationSchema = z.object({
+ id: z.number().min(1),
+ name: z.string().optional(),
+ email: z.string().email().optional(),
+});
+
+export type TAdminUpdateRecipientMutationSchema = z.infer<
+ typeof ZAdminUpdateRecipientMutationSchema
>;
-export const ZUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
+export const ZAdminUpdateSiteSettingMutationSchema = ZSiteSettingSchema;
-export type TUpdateSiteSettingMutationSchema = z.infer;
+export type TAdminUpdateSiteSettingMutationSchema = z.infer<
+ typeof ZAdminUpdateSiteSettingMutationSchema
+>;
+
+export const ZAdminResealDocumentMutationSchema = z.object({
+ id: z.number().min(1),
+});
+
+export type TAdminResealDocumentMutationSchema = z.infer;
+
+export const ZAdminDeleteUserMutationSchema = z.object({
+ id: z.number().min(1),
+ email: z.string().email(),
+});
+
+export type TAdminDeleteUserMutationSchema = z.infer;
diff --git a/packages/trpc/server/profile-router/router.ts b/packages/trpc/server/profile-router/router.ts
index f9f409aa6..542ac2807 100644
--- a/packages/trpc/server/profile-router/router.ts
+++ b/packages/trpc/server/profile-router/router.ts
@@ -207,9 +207,9 @@ export const profileRouter = router({
deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
- const user = ctx.user;
-
- return await deleteUser(user);
+ return await deleteUser({
+ id: ctx.user.id,
+ });
} catch (err) {
let message = 'We were unable to delete your account. Please try again.';