Skip to content

Commit

Permalink
Delete account backend integration (#264)
Browse files Browse the repository at this point in the history
* feat: delete account - WIP

* feat: delete account - add translations

* feat: delete account feature

* chore: update buildNumber

* feat: delete account - integration - wip

* feat: delete-account - done

---------

Co-authored-by: Andrew Radulescu <[email protected]>
Co-authored-by: Birloi Florian <[email protected]>
Co-authored-by: Birloi Florian <[email protected]>
  • Loading branch information
4 people authored Dec 19, 2023
1 parent bc7f35e commit 4c6ce9d
Show file tree
Hide file tree
Showing 17 changed files with 174 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ export class ActivityLogRepositoryService
return null;
}

async deleteMany(ids: string[]): Promise<void> {
await this.activityLogRepo.delete(ids);
async deleteManyByVolunteerId(volunteerId: string): Promise<void> {
await this.activityLogRepo.delete({ volunteerId });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class ActivityLogFacade {
return this.activityLogRepository.delete(id);
}

async deleteMany(ids: string[]): Promise<void> {
return this.activityLogRepository.deleteMany(ids);
async deleteManyByVolunteerId(volunteerId: string): Promise<void> {
return this.activityLogRepository.deleteManyByVolunteerId(volunteerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ContractRepositoryService

const query = this.contractRepository
.createQueryBuilder('contract')
.withDeleted()
.leftJoinAndMapOne(
'contract.organization',
'contract.organization',
Expand Down Expand Up @@ -187,6 +188,7 @@ export class ContractRepositoryService
template: true,
createdByAdmin: true,
},
withDeleted: true,
});

return ContractTransformer.fromEntity(contract);
Expand Down
5 changes: 5 additions & 0 deletions backend/src/modules/user/exceptions/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum UserExceptionCodes {
USER_004 = 'USER_004',
USER_005 = 'USER_005',
USER_006 = 'USER_006',
USER_007 = 'USER_007',
}

type UserExceptionCodeType = keyof typeof UserExceptionCodes;
Expand Down Expand Up @@ -40,4 +41,8 @@ export const UserExceptionMessages: Record<
code_error: UserExceptionCodes.USER_006,
message: 'Error while uploading profile picture in s3',
},
[UserExceptionCodes.USER_007]: {
code_error: UserExceptionCodes.USER_007,
message: 'Error while trying to delete user account',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ export class RegularUserRepositoryService implements IRegularUserRepository {
lastName: 'Deleted',
email: `account-deleted@${new Date().getTime()}.ro`,
phone: 'Deleted',
name: 'Deleted',
birthday: new Date(),
userPersonalDataId: null,
});

await this.regularUserRepository.save(userToUpdate);
const updated = await this.regularUserRepository.save(userToUpdate);

return this.find({ id });
await this.regularUserRepository.softDelete({ id: userToUpdate.id });

return updated ? RegularUserTransformer.fromEntity(updated) : null;
}
}
31 changes: 19 additions & 12 deletions backend/src/modules/volunteer/repositories/volunteer.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,34 +303,41 @@ export class VolunteerRepositoryService
});
}

async deleteManyAndProfiles(
async softDeleteManyAndProfiles(
userId: string,
): Promise<{ deletedProfiles: string[]; deletedVolunteers: string[] }> {
const volunteerRecords = await this.volunteerRepository.find({
where: { userId },
relations: { volunteerProfile: true },
});

let deletedProfiles;
// Anonimize emails before soft delete
await this.volunteerProfileRepository.update(
volunteerRecords.map((v) => v.volunteerProfile.id),
{
email: `account-deleted@${new Date().getTime()}.ro`,
},
const volunteerRecordsToDelete = volunteerRecords.filter(
(v) => v.volunteerProfile?.id,
);

// Soft Delete all associated profiles
const deletedProfiles = await this.volunteerProfileRepository.softRemove(
volunteerRecords.map((v) => v.volunteerProfile),
);
if (volunteerRecordsToDelete.length) {
await this.volunteerProfileRepository.update(
volunteerRecordsToDelete.map((v) => v.volunteerProfile.id),
{
email: `account-deleted@${new Date().getTime()}.ro`,
},
);

// Soft Delete all associated profiles
deletedProfiles = await this.volunteerProfileRepository.softRemove(
volunteerRecordsToDelete,
);
}

const deletedVolunteerRecords = await this.volunteerRepository.softRemove(
volunteerRecords,
);

return {
deletedProfiles: deletedProfiles.map((dp) => dp.id),
deletedVolunteers: deletedVolunteerRecords.map((dvr) => dvr.id),
deletedProfiles: deletedProfiles?.map((dp) => dp.id) || [],
deletedVolunteers: deletedVolunteerRecords?.map((dvr) => dvr.id) || [],
};
}

Expand Down
4 changes: 2 additions & 2 deletions backend/src/modules/volunteer/services/volunteer.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ export class VolunteerFacade {
return this.volunteerProfileRepositoryService.delete(id);
}

async deleteManyAndProfiles(
async softDeleteManyAndProfiles(
userId: string,
): Promise<{ deletedProfiles: string[]; deletedVolunteers: string[] }> {
return this.volunteerRepository.deleteManyAndProfiles(userId);
return this.volunteerRepository.softDeleteManyAndProfiles(userId);
}
}
66 changes: 37 additions & 29 deletions backend/src/usecases/user/delete-account.usecase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { IVolunteerModel } from 'src/modules/volunteer/model/volunteer.model';
import { ActivityLogFacade } from 'src/modules/activity-log/services/activity-log.facade';
import { AccessRequestFacade } from 'src/modules/access-request/services/access-request.facade';
import { EventFacade } from 'src/modules/event/services/event.facade';
import { JSONStringifyError } from 'src/common/helpers/utils';

@Injectable()
export class DeleteAccountRegularUserUsecase implements IUseCaseService<void> {
Expand Down Expand Up @@ -39,42 +40,49 @@ export class DeleteAccountRegularUserUsecase implements IUseCaseService<void> {
this.exceptionService.notFoundException(UserExceptionMessages.USER_001);
}

// // 1. Delete cognito user
await this.cognitoService.globalSignOut(user.cognitoId);
await this.cognitoService.deleteUser(user.cognitoId);
try {
// 1. Delete cognito user
await this.cognitoService.globalSignOut(user.cognitoId);
await this.cognitoService.deleteUser(user.cognitoId);

/* ======================================================= */
/* =========FULL IMPLEMENTATION UNTESTED ================= */
/* ======================================================= */
// 2. Hard delete all "PushTokens"
await this.pushNotificationService.deleteMany({ userId });

// // 2. Hard delete all "PushTokens"
// await this.pushNotificationService.deleteMany({ userId });
// // 3. Hard delete "UserPersonalData"
// if (user.userPersonalData?.id) {
// await this.userService.deleteUserPersonalData(user.userPersonalData.id);
// }
// 3. Soft delete all Volunteers Records and the associated Profiles for the given UserId
const deletedVolunteersAndProfiles =
await this.volunteerFacade.softDeleteManyAndProfiles(userId);

// // 4. Hard delete all Volunteers Records and the associated Profiles for the given UserId
// const deletedVolunteersAndProfiles =
// await this.volunteerFacade.deleteManyAndProfiles(userId);
// 4. Delete activity logs related to this user (linked with his Volunteer Records)
for (const volunteerId of deletedVolunteersAndProfiles.deletedVolunteers) {
await this.activityLogFacade.deleteManyByVolunteerId(volunteerId);
}

// // Delete activity logs related to this user
// await this.activityLogFacade.deleteMany(
// deletedVolunteersAndProfiles.deletedVolunteers,
// );
// 5. Delete all access requests made by the user
await this.accessRequestFacade.deleteAllForUser(userId);

// // Delete all access requests made by the user
// await this.accessRequestFacade.deleteAllForUser(userId);
// 6. Delete all RSVPs to events for the user
await this.eventFacade.deleteAllRSVPsForUser(userId);

// // Delete all RSVPs to events for the user
// await this.eventFacade.deleteAllRSVPsForUser(userId);
// 7. "User" - Anonimize + Soft delete + Delete profile picture
const deletedUser =
await this.userService.softDeleteAndAnonimizeRegularUser(userId);
if (deletedUser.profilePicture) {
await this.s3Service.deleteFile(deletedUser.profilePicture);
}

// // 4. "User" - Anonimize + Soft delete + Delete profile picture
// const deletedUser =
// await this.userService.softDeleteAndAnonimizeRegularUser(userId);
// if (deletedUser.profilePicture) {
// await this.s3Service.deleteFile(deletedUser.profilePicture);
// }
// 8. Hard delete "UserPersonalData"
if (user.userPersonalData?.id) {
await this.userService.deleteUserPersonalData(user.userPersonalData.id);
}
} catch (error) {
this.logger.error({
...UserExceptionMessages.USER_007,
error: JSONStringifyError(error),
});
this.exceptionService.internalServerErrorException(
UserExceptionMessages.USER_007,
);
}

return;
}
Expand Down
8 changes: 5 additions & 3 deletions mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const expoConfig: ExpoConfig = {
},
assetBundlePatterns: ['**/*'],
ios: {
buildNumber: '1',
buildNumber: '4',
supportsTablet: true,
bundleIdentifier: 'org.commitglobal.vic',
entitlements: {
Expand All @@ -32,7 +32,7 @@ const expoConfig: ExpoConfig = {
},
},
android: {
versionCode: 1,
versionCode: 3,
adaptiveIcon: {
foregroundImage: './src/assets/images/adaptive-icon.png',
backgroundColor: '#ffffff',
Expand All @@ -55,7 +55,8 @@ const expoConfig: ExpoConfig = {
[
'expo-image-picker',
{
photosPermission: 'The app accesses your photos to let you share them with your friends.',
photosPermission: 'The app accesses your photos to allow you to set a profile picture.',
cameraPermission: 'The app accesses your camera to allow you to set a profile picture.',
},
],
],
Expand All @@ -71,6 +72,7 @@ const expoConfig: ExpoConfig = {
policyLink: process.env.EXPO_PUBLIC_PRIVACY_POLICY_LINK,
termsLink: process.env.EXPO_PUBLIC_TERMS_AND_CONDITIONS_LINK,
infoLink: process.env.EXPO_PUBLIC_INFORMATION_LINK,
contactEmail: process.env.EXPO_PUBLIC_CONTACT_EMAIL,
},
updates: {
url: 'https://u.expo.dev/6aaad982-5a5c-4af8-b66c-7689afe74e1f',
Expand Down
9 changes: 8 additions & 1 deletion mobile/src/assets/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@
"password": "Change password",
"notification": "Notifications settings",
"information": "Information",
"logout": "Log out"
"logout": "Log out",
"delete": "Delete account"
},
"account_data": {
"title": "Date cont",
Expand Down Expand Up @@ -738,5 +739,11 @@
"rejected": "was rejected"
}
}
},
"delete_account": {
"title": "Confirm account deletion",
"paragraph": "To delete your account on the VIC application, please confirm your decision below. Deleting your account will result in the permanent loss of your data and access to the application. If you are certain about this action, click the 'Confirm Deletion' button. Keep in mind that this process is irreversible, and you will need to create a new account if you wish to use VIC in the future.",
"confirm": "Confirm Deletion",
"error": "There was an error trying to delete your account. Please contact us at {{value}} to finalize the process."
}
}
9 changes: 8 additions & 1 deletion mobile/src/assets/locales/ro/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@
"password": "Schimbă parola",
"notification": "Setări notificări",
"information": "Informații",
"logout": "Log out"
"logout": "Log out",
"delete": "Sterge cont"
},
"account_data": {
"title": "Date cont",
Expand Down Expand Up @@ -733,5 +734,11 @@
"rejected": "a fost respinsă"
}
}
},
"delete_account": {
"title": "Confirma ștergerea contului",
"paragraph": "Pentru a șterge contul tău în aplicația VIC, te rugăm să confirmi decizia mai jos. Ștergerea contului va duce la pierderea permanentă a datelor tale și a accesului la aplicație. Dacă ești sigur în privința acestei acțiuni, apasă butonul 'Confirmă Ștergerea'. Menționează că acest proces este ireversibil și va trebui să creezi un cont nou dacă dorești să utilizezi VIC în viitor.",
"confirm": "Confirmă Ștergerea",
"error": "A aparut o eroare la stergerea contului. Contacteaza-ne la adresa de e-mail {{value}}, si te vom ajuta sa finalizezi procesul."
}
}
3 changes: 3 additions & 0 deletions mobile/src/assets/svg/trash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 7L18.1327 19.1425C18.0579 20.1891 17.187 21 16.1378 21H7.86224C6.81296 21 5.94208 20.1891 5.86732 19.1425L5 7M10 11V17M14 11V17M15 7V4C15 3.44772 14.5523 3 14 3H10C9.44772 3 9 3.44772 9 4V7M4 7H20" stroke="#6B7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`;
2 changes: 2 additions & 0 deletions mobile/src/routes/Private.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import ContractRejectedReason from '../screens/ContractRejectedReason';
import PendingContracts from '../screens/PendingContracts';
import ContractHistory from '../screens/ContractHistory';
import RequestRejectedReason from '../screens/RequestRejectedReason';
import DeleteAccount from '../screens/DeleteAccount';

const { Navigator, Screen, Group } = createNativeStackNavigator();

Expand Down Expand Up @@ -60,6 +61,7 @@ const Private = () => (
<Screen name="create-volunteer" component={CreateVolunteer} />
<Screen name="rejected-contract" component={ContractRejectedReason} />
<Screen name="rejected-request" component={RequestRejectedReason} />
<Screen name="delete-account" component={DeleteAccount} />
</Group>
</Navigator>
);
Expand Down
60 changes: 60 additions & 0 deletions mobile/src/screens/DeleteAccount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import ModalLayout from '../layouts/ModalLayout';
import { Text, useTheme } from '@ui-kitten/components';
import { ALLOW_FONT_SCALLING } from '../common/constants/constants';
import { ButtonType } from '../common/enums/button-type.enum';
import { useTranslation } from 'react-i18next';
import FormLayout from '../layouts/FormLayout';
import { useDeleteAccountMutation } from '../services/user/user.service';
import { useAuth } from '../hooks/useAuth';
import Constants from 'expo-constants';

const DeleteAccount = ({ navigation }: any) => {
const { t } = useTranslation('delete_account');
const { logout } = useAuth();
const theme = useTheme();

const {
mutate: deleteAccount,
isLoading: isDeletingAccount,
error: deleteAccountError,
} = useDeleteAccountMutation();

const onConfirmDeleteAccount = () => {
deleteAccount(undefined, {
onSuccess: () => {
logout();
},
});
};

return (
<ModalLayout
title={t('title')}
actionsOptions={{
buttonType: ButtonType.DANGER,
onActionButtonClick: onConfirmDeleteAccount,
actionLabel: t('confirm'),
loading: isDeletingAccount,
}}
onDismiss={navigation.goBack}
>
<FormLayout>
<Text allowFontScaling={ALLOW_FONT_SCALLING} category="p1">
{`${t('paragraph')}`}
</Text>
{!!deleteAccountError && (
<Text
allowFontScaling={ALLOW_FONT_SCALLING}
style={{ color: theme['color-danger-500'] }}
category="p1"
>
{`${t('error', { value: Constants.expoConfig?.extra?.contactEmail })}`}
</Text>
)}
</FormLayout>
</ModalLayout>
);
};

export default DeleteAccount;
Loading

0 comments on commit 4c6ce9d

Please sign in to comment.