Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: upstream sync #109

Merged
merged 22 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f652ca9
feat: account deletion confirmation dialog
ephraimduncan Jan 20, 2024
a3e5608
feat: delete user from db and unsubscribe from stripe
ephraimduncan Jan 20, 2024
7762b1d
feat: add loading to button
ephraimduncan Jan 21, 2024
9e433af
feat: require 2fa code before account is deleted
ephraimduncan Jan 21, 2024
014c09b
fix: account deletion error for users without two factor authentication
ephraimduncan Jan 28, 2024
d598677
feat: add transparent background to fields
ephraimduncan Feb 2, 2024
4d93ed6
feat: add deleted account migration
ephraimduncan Feb 5, 2024
4c09867
feat: transfer completed and pending documents to deleted email
ephraimduncan Feb 5, 2024
bc98907
Merge branch 'main' into feat/account-deletion
ephraimduncan Feb 5, 2024
3075281
feat: soft-delete transfered documents
ephraimduncan Feb 5, 2024
7ef7715
Merge branch 'main' into transparent-fields
adithyaakrishna Feb 7, 2024
6daaa3a
Merge branch 'main' into feat/account-deletion
ephraimduncan Feb 14, 2024
cab875f
fix: update create delete user sql script
ephraimduncan Feb 14, 2024
c680cfc
chore: update pr based on review
ephraimduncan Feb 14, 2024
fddd860
chore: code refactor to avoid repetitions
ephraimduncan Feb 15, 2024
f98567e
feat: request usee to disable 2fa before deleting account
ephraimduncan Feb 17, 2024
7226d5a
Merge branch 'main' into feat/account-deletion
Mythie Feb 24, 2024
9cf72e1
chore: tidy code and extract alert-dialog
Mythie Feb 25, 2024
df3ba11
feat: account deletion (#846)
Mythie Feb 25, 2024
5bef2fb
fix: follow figma design
Mythie Feb 25, 2024
187a988
Merge branch 'main' into transparent-fields
Mythie Feb 25, 2024
6ea6dda
feat: add transparent background to fields (#899)
Mythie Feb 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
"ghcr.io/devcontainers/features/node:1": {}
},
"onCreateCommand": "./.devcontainer/on-create.sh",
"forwardPorts": [3000, 54320, 9000, 2500, 1100],
"forwardPorts": [
3000,
54320,
9000,
2500,
1100
],
"customizations": {
"vscode": {
"extensions": [
Expand All @@ -25,8 +31,8 @@
"GitHub.copilot",
"GitHub.vscode-pull-request-github",
"Prisma.prisma",
"VisualStudioExptTeam.vscodeintellicode",
"VisualStudioExptTeam.vscodeintellicode"
]
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
'use client';

import { signOut } from 'next-auth/react';

import type { User } from '@documenso/prisma/client';
import { TRPCClientError } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@documenso/ui/primitives/dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';

export type DeleteAccountDialogProps = {
className?: string;
user: User;
};

export const DeleteAccountDialog = ({ className, user }: DeleteAccountDialogProps) => {
const { toast } = useToast();

const hasTwoFactorAuthentication = user.twoFactorEnabled;

const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();

const onDeleteAccount = async () => {
try {
await deleteAccount();

toast({
title: 'Account deleted',
description: 'Your account has been deleted successfully.',
duration: 5000,
});

return await signOut({ callbackUrl: '/' });
} catch (err) {
if (err instanceof TRPCClientError && err.data?.code === 'BAD_REQUEST') {
toast({
title: 'An error occurred',
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: 'An unknown error occurred',
variant: 'destructive',
description:
err.message ??
'We encountered an unknown error while attempting to delete your account. Please try again later.',
});
}
}
};

return (
<div className={className}>
<Alert
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row "
variant="neutral"
>
<div>
<AlertTitle>Delete Account</AlertTitle>
<AlertDescription className="mr-2">
Delete your account and all its contents, including completed documents. This action is
irreversible and will cancel your subscription, so proceed with caution.
</AlertDescription>
</div>

<div className="flex-shrink-0">
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">Delete Account</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="space-y-4">
<DialogTitle>Delete Account</DialogTitle>

<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
This action is not reversible. Please be certain.
</AlertDescription>
</Alert>

{hasTwoFactorAuthentication && (
<Alert variant="destructive">
<AlertDescription className="selection:bg-red-100">
Disable Two Factor Authentication before deleting your account.
</AlertDescription>
</Alert>
)}

<DialogDescription>
Documenso will delete <span className="font-semibold">all of your documents</span>
, along with all of your completed documents, signatures, and all other resources
belonging to your Account.
</DialogDescription>
</DialogHeader>

<DialogFooter>
<Button
onClick={onDeleteAccount}
loading={isDeletingAccount}
variant="destructive"
disabled={hasTwoFactorAuthentication}
>
{isDeletingAccount ? 'Deleting account...' : 'Delete Account'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Alert>
</div>
);
};
6 changes: 5 additions & 1 deletion apps/web/src/app/(dashboard)/settings/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-
import { SettingsHeader } from '~/components/(dashboard)/settings/layout/header';
import { ProfileForm } from '~/components/forms/profile';

import { DeleteAccountDialog } from './delete-account-dialog';

export const metadata: Metadata = {
title: 'Profile',
};
Expand All @@ -16,7 +18,9 @@ export default async function ProfileSettingsPage() {
<div>
<SettingsHeader title="Profile" subtitle="Here you can edit your personal details." />

<ProfileForm user={user} className="max-w-xl" />
<ProfileForm className="max-w-xl" user={user} />

<DeleteAccountDialog className="mt-8 max-w-xl" user={user} />
</div>
);
}
10 changes: 9 additions & 1 deletion apps/web/src/components/forms/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export const ZProfileFormSchema = z.object({
signature: z.string().min(1, 'Signature Pad cannot be empty'),
});

export const ZTwoFactorAuthTokenSchema = z.object({
token: z.string(),
});

export type TTwoFactorAuthTokenSchema = z.infer<typeof ZTwoFactorAuthTokenSchema>;
export type TProfileFormSchema = z.infer<typeof ZProfileFormSchema>;

export type ProfileFormProps = {
Expand All @@ -50,8 +55,11 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
});

const isSubmitting = form.formState.isSubmitting;
const hasTwoFactorAuthentication = user.twoFactorEnabled;

const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation();
const { mutateAsync: deleteAccount, isLoading: isDeletingAccount } =
trpc.profile.deleteAccount.useMutation();

const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => {
try {
Expand Down Expand Up @@ -133,7 +141,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
/>
</fieldset>

<Button type="submit" loading={isSubmitting}>
<Button type="submit" loading={isSubmitting} className="self-end">
{isSubmitting ? 'Updating profile...' : 'Update profile'}
</Button>
</form>
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/server-only/2fa/validate-2fa.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { User } from '@documenso/prisma/client';
import type { User } from '@documenso/prisma/client';

import { ErrorCode } from '../../next-auth/error-codes';
import { verifyTwoFactorAuthenticationToken } from './verify-2fa-token';
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/server-only/2fa/verify-2fa-token.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { base32 } from '@scure/base';
import { TOTPController } from 'oslo/otp';

import { User } from '@documenso/prisma/client';
import type { User } from '@documenso/prisma/client';

import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricDecrypt } from '../../universal/crypto';
Expand Down
19 changes: 19 additions & 0 deletions packages/lib/server-only/user/delete-user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { prisma } from '@documenso/prisma';
import { DocumentStatus } from '@documenso/prisma/client';

import { deletedAccountServiceAccount } from './service-accounts/deleted-account';

export type DeleteUserOptions = {
email: string;
Expand All @@ -17,6 +20,22 @@ export const deleteUser = async ({ email }: DeleteUserOptions) => {
throw new Error(`User with email ${email} not found`);
}

const serviceAccount = await deletedAccountServiceAccount();

// TODO: Send out cancellations for all pending docs
await prisma.document.updateMany({
where: {
userId: user.id,
status: {
in: [DocumentStatus.PENDING, DocumentStatus.COMPLETED],
},
},
data: {
userId: serviceAccount.id,
deletedAt: new Date(),
},
});

return await prisma.user.delete({
where: {
id: user.id,
Expand Down
17 changes: 17 additions & 0 deletions packages/lib/server-only/user/service-accounts/deleted-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { prisma } from '@documenso/prisma';

export const deletedAccountServiceAccount = async () => {
const serviceAccount = await prisma.user.findFirst({
where: {
email: '[email protected]',
},
});

if (!serviceAccount) {
throw new Error(
'Deleted account service account not found, have you ran the appropriate migrations?',
);
}

return serviceAccount;
Comment on lines +3 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider externalizing the email address '[email protected]' to an environment variable for easier configuration and to avoid hardcoding values in the codebase. Additionally, enhancing the error message to include steps for running migrations could improve developer experience.

-      email: '[email protected]',
+      email: process.env.DELETED_ACCOUNT_SERVICE_EMAIL,

And for the error message:

-      'Deleted account service account not found, have you ran the appropriate migrations?',
+      'Deleted account service account not found. Please ensure you have run the appropriate migrations and that the DELETED_ACCOUNT_SERVICE_EMAIL environment variable is correctly set.',

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-- Create [email protected]
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM "public"."User" WHERE "email" = '[email protected]') THEN
INSERT INTO
"public"."User" (
"email",
"emailVerified",
"password",
"createdAt",
"updatedAt",
"lastSignedIn",
"roles",
"identityProvider",
"twoFactorEnabled"
)
VALUES
(
'[email protected]',
NOW(),
NULL,
NOW(),
NOW(),
NOW(),
ARRAY['USER'::TEXT]::"public"."Role" [],
CAST('GOOGLE'::TEXT AS "public"."IdentityProvider"),
FALSE
);
END IF;
END $$
5 changes: 5 additions & 0 deletions packages/tailwind-config/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ module.exports = {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
'field-card': {
DEFAULT: 'hsl(var(--field-card))',
border: 'hsl(var(--field-card-border))',
foreground: 'hsl(var(--field-card-foreground))',
},
widget: {
DEFAULT: 'hsl(var(--widget))',
// foreground: 'hsl(var(--widget-foreground))',
Expand Down
2 changes: 1 addition & 1 deletion packages/trpc/react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import SuperJSON from 'superjson';

import { getBaseUrl } from '@documenso/lib/universal/get-base-url';

import { AppRouter } from '../server/router';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCReact<AppRouter>({
unstable_overrides: {
Expand Down
20 changes: 20 additions & 0 deletions packages/trpc/server/profile-router/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TRPCError } from '@trpc/server';

import { deleteUser } from '@documenso/lib/server-only/user/delete-user';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The deleteUser function implementation primarily focuses on transferring documents associated with the user to a service account and then deleting the user record. However, there are a few considerations for ensuring comprehensive data deletion:

  • The presence of a TODO comment suggests that the functionality to send out cancellations for all pending documents is not yet implemented, indicating the function might not fully address all aspects of user data.
  • The function does not explicitly mention handling other user-related data (e.g., sessions, preferences), which might be necessary for a thorough account deletion process.

Given these observations, it's recommended to review the deleteUser function further to ensure it comprehensively handles all aspects of user data deletion, including the completion of the noted TODO and consideration of other user-related data.

Analysis chain

The import of deleteUser from @documenso/lib/server-only/user/delete-user is crucial for enabling account deletion functionality. It's important to ensure that the deleteUser function properly handles all aspects of user data deletion, including associated documents and any other related data, to maintain data integrity and privacy.


Scripts Executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Verify the implementation of deleteUser function to ensure comprehensive data deletion
echo "Manual verification required for the deleteUser function implementation."

Length of output: 154



Script:

#!/bin/bash
# Attempt to locate and display the contents of the deleteUser function implementation
fd "delete-user\.(js|ts)" --exec cat {}

Length of output: 1014

import { findUserSecurityAuditLogs } from '@documenso/lib/server-only/user/find-user-security-audit-logs';
import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password';
import { getUserById } from '@documenso/lib/server-only/user/get-user-by-id';
Expand Down Expand Up @@ -155,4 +156,23 @@ export const profileRouter = router({
});
}
}),

deleteAccount: authenticatedProcedure.mutation(async ({ ctx }) => {
try {
const user = ctx.user;

return await deleteUser(user);
} catch (err) {
let message = 'We were unable to delete your account. Please try again.';

if (err instanceof Error) {
message = err.message;
}

throw new TRPCError({
code: 'BAD_REQUEST',
message,
});
}
}),
});
3 changes: 2 additions & 1 deletion packages/ui/primitives/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const buttonVariants = cva(
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive',
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/primitives/document-flow/add-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,9 @@ export const AddFieldsFormPartial = ({
{selectedField && (
<Card
className={cn(
'bg-background pointer-events-none fixed z-50 cursor-pointer transition-opacity',
'bg-field-card/80 pointer-events-none fixed z-50 cursor-pointer border-2 backdrop-blur-[1px]',
{
'border-primary': isFieldWithinBounds,
'border-field-card-border': isFieldWithinBounds,
'opacity-50': !isFieldWithinBounds,
},
)}
Expand All @@ -336,7 +336,7 @@ export const AddFieldsFormPartial = ({
width: fieldBounds.current.width,
}}
>
<CardContent className="text-foreground flex h-full w-full items-center justify-center p-2">
<CardContent className="text-field-card-foreground flex h-full w-full items-center justify-center p-2">
{FRIENDLY_FIELD_TYPE[selectedField]}
</CardContent>
</Card>
Expand Down
Loading
Loading