mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 00:52:21 +03:00
feat: I can delete my account easily (#977)
* Add support for account deletion Co-authored-by: v1b3m <vibenjamin6@gmail.com> * Add more fixes Co-authored-by: Benjamin Mayanja <vibenjamin6@gmail.com> * Add more fixes Co-authored-by: v1b3m <vibenjamin6@gmail.com> --------- Co-authored-by: v1b3m <vibenjamin6@gmail.com>
This commit is contained in:
parent
3daebd0e0c
commit
d142376ef9
@ -894,6 +894,7 @@ export type Mutation = {
|
|||||||
deleteManyCompany: AffectedRows;
|
deleteManyCompany: AffectedRows;
|
||||||
deleteManyPerson: AffectedRows;
|
deleteManyPerson: AffectedRows;
|
||||||
deleteManyPipelineProgress: AffectedRows;
|
deleteManyPipelineProgress: AffectedRows;
|
||||||
|
deleteUserAccount: User;
|
||||||
deleteWorkspaceMember: WorkspaceMember;
|
deleteWorkspaceMember: WorkspaceMember;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
signUp: LoginToken;
|
signUp: LoginToken;
|
||||||
@ -2588,6 +2589,11 @@ export type RemoveProfilePictureMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type RemoveProfilePictureMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: string, avatarUrl?: string | null } };
|
export type RemoveProfilePictureMutation = { __typename?: 'Mutation', updateUser: { __typename?: 'User', id: string, avatarUrl?: string | null } };
|
||||||
|
|
||||||
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type DeleteUserAccountMutation = { __typename?: 'Mutation', deleteUserAccount: { __typename?: 'User', id: string } };
|
||||||
|
|
||||||
export type GetViewFieldsQueryVariables = Exact<{
|
export type GetViewFieldsQueryVariables = Exact<{
|
||||||
where?: InputMaybe<ViewFieldWhereInput>;
|
where?: InputMaybe<ViewFieldWhereInput>;
|
||||||
}>;
|
}>;
|
||||||
@ -4780,6 +4786,38 @@ export function useRemoveProfilePictureMutation(baseOptions?: Apollo.MutationHoo
|
|||||||
export type RemoveProfilePictureMutationHookResult = ReturnType<typeof useRemoveProfilePictureMutation>;
|
export type RemoveProfilePictureMutationHookResult = ReturnType<typeof useRemoveProfilePictureMutation>;
|
||||||
export type RemoveProfilePictureMutationResult = Apollo.MutationResult<RemoveProfilePictureMutation>;
|
export type RemoveProfilePictureMutationResult = Apollo.MutationResult<RemoveProfilePictureMutation>;
|
||||||
export type RemoveProfilePictureMutationOptions = Apollo.BaseMutationOptions<RemoveProfilePictureMutation, RemoveProfilePictureMutationVariables>;
|
export type RemoveProfilePictureMutationOptions = Apollo.BaseMutationOptions<RemoveProfilePictureMutation, RemoveProfilePictureMutationVariables>;
|
||||||
|
export const DeleteUserAccountDocument = gql`
|
||||||
|
mutation DeleteUserAccount {
|
||||||
|
deleteUserAccount {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type DeleteUserAccountMutationFn = Apollo.MutationFunction<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDeleteUserAccountMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useDeleteUserAccountMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDeleteUserAccountMutation` returns a tuple that includes:
|
||||||
|
* - A mutate function that you can call at any time to execute the mutation
|
||||||
|
* - An object with fields that represent the current status of the mutation's execution
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const [deleteUserAccountMutation, { data, loading, error }] = useDeleteUserAccountMutation({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>(DeleteUserAccountDocument, options);
|
||||||
|
}
|
||||||
|
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
|
||||||
|
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
|
||||||
|
export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
|
||||||
export const GetViewFieldsDocument = gql`
|
export const GetViewFieldsDocument = gql`
|
||||||
query GetViewFields($where: ViewFieldWhereInput) {
|
query GetViewFields($where: ViewFieldWhereInput) {
|
||||||
viewFields: findManyViewField(where: $where) {
|
viewFields: findManyViewField(where: $where) {
|
||||||
|
106
front/src/modules/settings/profile/components/DeleteModal.tsx
Normal file
106
front/src/modules/settings/profile/components/DeleteModal.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { Button, ButtonVariant } from '@/ui/button/components/Button';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { Modal } from '@/ui/modal/components/Modal';
|
||||||
|
import { debounce } from '~/utils/debounce';
|
||||||
|
|
||||||
|
interface DeleteModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
setIsOpen: (val: boolean) => void;
|
||||||
|
handleConfirmDelete: () => void;
|
||||||
|
deleteButtonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledTitle = styled.div`
|
||||||
|
font-size: ${({ theme }) => theme.font.size.lg};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledModal = styled(Modal)`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
> * + * {
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(8)};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCenteredButton = styled(Button)`
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledDeleteButton = styled(StyledCenteredButton)`
|
||||||
|
border-color: ${({ theme }) => theme.color.red20};
|
||||||
|
color: ${({ theme }) => theme.color.red};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function DeleteModal({
|
||||||
|
isOpen = false,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
setIsOpen,
|
||||||
|
handleConfirmDelete,
|
||||||
|
deleteButtonText = 'Delete',
|
||||||
|
}: DeleteModalProps) {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||||
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
const userEmail = currentUser?.email;
|
||||||
|
|
||||||
|
const handleEmailChange = (val: string) => {
|
||||||
|
setEmail(val);
|
||||||
|
isEmailMatchingUserEmail(val, userEmail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEmailMatchingUserEmail = debounce(
|
||||||
|
(email1?: string, email2?: string) => {
|
||||||
|
setIsValidEmail(Boolean(email1 && email2 && email1 === email2));
|
||||||
|
},
|
||||||
|
250,
|
||||||
|
);
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
email && !isValidEmail ? 'email provided is not correct' : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<LayoutGroup>
|
||||||
|
<StyledModal isOpen={isOpen}>
|
||||||
|
<StyledTitle>{title}</StyledTitle>
|
||||||
|
<div>{subtitle}</div>
|
||||||
|
<TextInput
|
||||||
|
value={email}
|
||||||
|
onChange={handleEmailChange}
|
||||||
|
placeholder={userEmail}
|
||||||
|
fullWidth
|
||||||
|
key={'email-' + userEmail}
|
||||||
|
error={errorMessage}
|
||||||
|
/>
|
||||||
|
<StyledDeleteButton
|
||||||
|
onClick={handleConfirmDelete}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
title={deleteButtonText}
|
||||||
|
disabled={!isValidEmail || !email}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<StyledCenteredButton
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
variant={ButtonVariant.Secondary}
|
||||||
|
title="Cancel"
|
||||||
|
fullWidth
|
||||||
|
style={{
|
||||||
|
marginTop: 10,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledModal>
|
||||||
|
</LayoutGroup>
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
}
|
@ -1,51 +1,25 @@
|
|||||||
import { useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { AnimatePresence, LayoutGroup } from 'framer-motion';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
|
|
||||||
import { useAuth } from '@/auth/hooks/useAuth';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { Button, ButtonVariant } from '@/ui/button/components/Button';
|
import { ButtonVariant } from '@/ui/button/components/Button';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import { Modal } from '@/ui/modal/components/Modal';
|
|
||||||
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/title/components/SubSectionTitle';
|
||||||
import { useDeleteCurrentWorkspaceMutation } from '~/generated/graphql';
|
import {
|
||||||
import { debounce } from '~/utils/debounce';
|
useDeleteCurrentWorkspaceMutation,
|
||||||
|
useDeleteUserAccountMutation,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledCenteredButton = styled(Button)`
|
import { DeleteModal, StyledDeleteButton } from './DeleteModal';
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDeleteButton = styled(StyledCenteredButton)`
|
|
||||||
border-color: ${({ theme }) => theme.color.red20};
|
|
||||||
color: ${({ theme }) => theme.color.red};
|
|
||||||
font-size: ${({ theme }) => theme.font.size.md};
|
|
||||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTitle = styled.div`
|
|
||||||
font-size: ${({ theme }) => theme.font.size.lg};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledModal = styled(Modal)`
|
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
|
||||||
> * + * {
|
|
||||||
margin-top: ${({ theme }) => theme.spacing(8)};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function DeleteWorkspace() {
|
export function DeleteWorkspace() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isDeleteWorkSpaceModalOpen, setIsDeleteWorkSpaceModalOpen] =
|
||||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [isDeleteAccountModalOpen, setIsDeleteAccountModalOpen] =
|
||||||
const currentUser = useRecoilValue(currentUserState);
|
useState(false);
|
||||||
const userEmail = currentUser?.email;
|
|
||||||
|
|
||||||
const [deleteCurrentWorkspace] = useDeleteCurrentWorkspaceMutation();
|
const [deleteCurrentWorkspace] = useDeleteCurrentWorkspaceMutation();
|
||||||
|
const [deleteUserAccount] = useDeleteUserAccountMutation();
|
||||||
const { signOut } = useAuth();
|
const { signOut } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -59,20 +33,15 @@ export function DeleteWorkspace() {
|
|||||||
handleLogout();
|
handleLogout();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEmailMatchingUserEmail = debounce(
|
const deleteAccount = async () => {
|
||||||
(email1?: string, email2?: string) => {
|
await deleteUserAccount();
|
||||||
setIsValidEmail(Boolean(email1 && email2 && email1 === email2));
|
handleLogout();
|
||||||
},
|
|
||||||
250,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEmailChange = (val: string) => {
|
|
||||||
setEmail(val);
|
|
||||||
isEmailMatchingUserEmail(val, userEmail);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorMessage =
|
const subtitle = (
|
||||||
email && !isValidEmail ? 'email provided is not correct' : '';
|
type: 'workspace' | 'account',
|
||||||
|
) => `This action cannot be undone. This will permanently delete your
|
||||||
|
entire ${type}. Please type in your email to confirm.`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -81,46 +50,38 @@ export function DeleteWorkspace() {
|
|||||||
description="Delete your whole workspace"
|
description="Delete your whole workspace"
|
||||||
/>
|
/>
|
||||||
<StyledDeleteButton
|
<StyledDeleteButton
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsDeleteWorkSpaceModalOpen(true)}
|
||||||
variant={ButtonVariant.Secondary}
|
variant={ButtonVariant.Secondary}
|
||||||
title="Delete workspace"
|
title="Delete workspace"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<SubSectionTitle
|
||||||
<LayoutGroup>
|
title=""
|
||||||
<StyledModal isOpen={isOpen}>
|
description="Delete account and all the associated data"
|
||||||
<StyledTitle>Workspace Deletion</StyledTitle>
|
/>
|
||||||
<div>
|
<StyledDeleteButton
|
||||||
This action cannot be undone. This will permanently delete your
|
onClick={() => setIsDeleteAccountModalOpen(true)}
|
||||||
entire workspace. Please type in your email to confirm.
|
variant={ButtonVariant.Secondary}
|
||||||
</div>
|
title="Delete account"
|
||||||
<TextInput
|
/>
|
||||||
value={email}
|
|
||||||
onChange={handleEmailChange}
|
<DeleteModal
|
||||||
placeholder={userEmail}
|
isOpen={isDeleteWorkSpaceModalOpen}
|
||||||
fullWidth
|
setIsOpen={setIsDeleteWorkSpaceModalOpen}
|
||||||
key={'email-' + userEmail}
|
title="Workspace Deletion"
|
||||||
error={errorMessage}
|
subtitle={subtitle('workspace')}
|
||||||
/>
|
handleConfirmDelete={deleteWorkspace}
|
||||||
<StyledDeleteButton
|
deleteButtonText="Delete workspace"
|
||||||
onClick={deleteWorkspace}
|
/>
|
||||||
variant={ButtonVariant.Secondary}
|
|
||||||
title="Delete workspace"
|
<DeleteModal
|
||||||
disabled={!isValidEmail || !email}
|
isOpen={isDeleteAccountModalOpen}
|
||||||
fullWidth
|
setIsOpen={setIsDeleteAccountModalOpen}
|
||||||
/>
|
title="Account Deletion"
|
||||||
<StyledCenteredButton
|
subtitle={subtitle('account')}
|
||||||
onClick={() => setIsOpen(false)}
|
handleConfirmDelete={deleteAccount}
|
||||||
variant={ButtonVariant.Secondary}
|
deleteButtonText="Delete account"
|
||||||
title="Cancel"
|
/>
|
||||||
fullWidth
|
|
||||||
style={{
|
|
||||||
marginTop: 10,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledModal>
|
|
||||||
</LayoutGroup>
|
|
||||||
</AnimatePresence>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -42,3 +42,11 @@ export const REMOVE_PROFILE_PICTURE = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const DELETE_USER_ACCOUNT = gql`
|
||||||
|
mutation DeleteUserAccount {
|
||||||
|
deleteUserAccount {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@ -59,7 +59,7 @@ export class AbilityFactory {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
can(AbilityAction.Update, 'User', { id: user.id });
|
can(AbilityAction.Update, 'User', { id: user.id });
|
||||||
cannot(AbilityAction.Delete, 'User');
|
can(AbilityAction.Delete, 'User', { id: user.id });
|
||||||
|
|
||||||
// Workspace
|
// Workspace
|
||||||
can(AbilityAction.Read, 'Workspace');
|
can(AbilityAction.Read, 'Workspace');
|
||||||
|
@ -65,6 +65,7 @@ export class UpdateUserAbilityHandler implements IAbilityHandler {
|
|||||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||||
const gqlContext = GqlExecutionContext.create(context);
|
const gqlContext = GqlExecutionContext.create(context);
|
||||||
const args = gqlContext.getArgs<UserArgs>();
|
const args = gqlContext.getArgs<UserArgs>();
|
||||||
|
// TODO: Confirm if this is correct
|
||||||
const user = await this.prismaService.client.user.findFirst({
|
const user = await this.prismaService.client.user.findFirst({
|
||||||
where: args.where,
|
where: args.where,
|
||||||
});
|
});
|
||||||
@ -92,8 +93,14 @@ export class DeleteUserAbilityHandler implements IAbilityHandler {
|
|||||||
async handle(ability: AppAbility, context: ExecutionContext) {
|
async handle(ability: AppAbility, context: ExecutionContext) {
|
||||||
const gqlContext = GqlExecutionContext.create(context);
|
const gqlContext = GqlExecutionContext.create(context);
|
||||||
const args = gqlContext.getArgs<UserArgs>();
|
const args = gqlContext.getArgs<UserArgs>();
|
||||||
|
|
||||||
|
// obtain the auth user from the context
|
||||||
|
const reqUser = gqlContext.getContext().req.user;
|
||||||
|
|
||||||
|
// FIXME: When `args.where` is undefined(which it is in almost all the cases I've tested),
|
||||||
|
// this query will return the first user entry in the DB, which is most likely not the current user
|
||||||
const user = await this.prismaService.client.user.findFirst({
|
const user = await this.prismaService.client.user.findFirst({
|
||||||
where: args.where,
|
where: { ...args.where, id: reqUser.user.id },
|
||||||
});
|
});
|
||||||
assert(user, '', NotFoundException);
|
assert(user, '', NotFoundException);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
|
|
||||||
import { accessibleBy } from '@casl/prisma';
|
import { accessibleBy } from '@casl/prisma';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma, Workspace } from '@prisma/client';
|
||||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||||
|
|
||||||
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
|
import { FileFolder } from 'src/core/file/interfaces/file-folder.interface';
|
||||||
@ -25,6 +25,7 @@ import {
|
|||||||
import { AbilityGuard } from 'src/guards/ability.guard';
|
import { AbilityGuard } from 'src/guards/ability.guard';
|
||||||
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
|
||||||
import {
|
import {
|
||||||
|
DeleteUserAbilityHandler,
|
||||||
ReadUserAbilityHandler,
|
ReadUserAbilityHandler,
|
||||||
UpdateUserAbilityHandler,
|
UpdateUserAbilityHandler,
|
||||||
} from 'src/ability/handlers/user.ability-handler';
|
} from 'src/ability/handlers/user.ability-handler';
|
||||||
@ -35,6 +36,7 @@ import { assert } from 'src/utils/assert';
|
|||||||
import { UpdateOneUserArgs } from 'src/core/@generated/user/update-one-user.args';
|
import { UpdateOneUserArgs } from 'src/core/@generated/user/update-one-user.args';
|
||||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||||
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
import { FileUploadService } from 'src/core/file/services/file-upload.service';
|
||||||
|
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
|
||||||
|
|
||||||
import { UserService } from './user.service';
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
@ -147,4 +149,14 @@ export class UserResolver {
|
|||||||
|
|
||||||
return paths[0];
|
return paths[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => User)
|
||||||
|
@UseGuards(AbilityGuard)
|
||||||
|
@CheckAbilities(DeleteUserAbilityHandler)
|
||||||
|
async deleteUserAccount(
|
||||||
|
@AuthUser() { id: userId }: User,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
return this.userService.deleteUser({ userId, workspaceId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,4 +91,89 @@ export class UserService {
|
|||||||
|
|
||||||
return user as Prisma.UserGetPayload<T>;
|
return user as Prisma.UserGetPayload<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteUser({
|
||||||
|
workspaceId,
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
userId: string;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
workspaceMember,
|
||||||
|
company,
|
||||||
|
comment,
|
||||||
|
attachment,
|
||||||
|
refreshToken,
|
||||||
|
activity,
|
||||||
|
activityTarget,
|
||||||
|
} = this.prismaService.client;
|
||||||
|
const user = await this.findUnique({
|
||||||
|
where: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(user, 'User not found');
|
||||||
|
|
||||||
|
const workspace = await this.workspaceService.findUnique({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
assert(workspace, 'Workspace not found');
|
||||||
|
|
||||||
|
const workSpaceMembers = await workspaceMember.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isLastMember =
|
||||||
|
workSpaceMembers.length === 1 && workSpaceMembers[0].userId === userId;
|
||||||
|
|
||||||
|
if (isLastMember) {
|
||||||
|
// Delete entire workspace
|
||||||
|
await this.workspaceService.deleteWorkspace({
|
||||||
|
workspaceId,
|
||||||
|
userId,
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const where = { authorId: userId };
|
||||||
|
const activities = await activity.findMany({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prismaService.client.$transaction([
|
||||||
|
workspaceMember.deleteMany({
|
||||||
|
where: { userId },
|
||||||
|
}),
|
||||||
|
company.deleteMany({
|
||||||
|
where: { accountOwnerId: userId },
|
||||||
|
}),
|
||||||
|
comment.deleteMany({
|
||||||
|
where,
|
||||||
|
}),
|
||||||
|
attachment.deleteMany({
|
||||||
|
where,
|
||||||
|
}),
|
||||||
|
refreshToken.deleteMany({
|
||||||
|
where: { userId },
|
||||||
|
}),
|
||||||
|
...activities.map(({ id: activityId }) =>
|
||||||
|
activityTarget.deleteMany({
|
||||||
|
where: { activityId },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
activity.deleteMany({
|
||||||
|
where,
|
||||||
|
}),
|
||||||
|
this.delete({ where: { id: userId } }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user