From 4c6ce9d52d4a6d00e0c638dace79ffca25c9745d Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 19 Dec 2023 14:05:38 +0200 Subject: [PATCH] Delete account backend integration (#264) * 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 Co-authored-by: Birloi Florian Co-authored-by: Birloi Florian --- .../repositories/activity-log.repository.ts | 4 +- .../services/activity-log.facade.ts | 4 +- .../repositories/contract.repository.ts | 2 + .../src/modules/user/exceptions/exceptions.ts | 5 ++ .../repositories/regular-user.repository.ts | 9 ++- .../repositories/volunteer.repository.ts | 31 +++++---- .../volunteer/services/volunteer.facade.ts | 4 +- .../usecases/user/delete-account.usecase.ts | 66 +++++++++++-------- mobile/app.config.ts | 8 ++- mobile/src/assets/locales/en/translation.json | 9 ++- mobile/src/assets/locales/ro/translation.json | 9 ++- mobile/src/assets/svg/trash.js | 3 + mobile/src/routes/Private.tsx | 2 + mobile/src/screens/DeleteAccount.tsx | 60 +++++++++++++++++ mobile/src/screens/Settings.tsx | 3 + mobile/src/services/user/user.api.ts | 4 ++ mobile/src/services/user/user.service.ts | 5 ++ 17 files changed, 174 insertions(+), 54 deletions(-) create mode 100644 mobile/src/assets/svg/trash.js create mode 100644 mobile/src/screens/DeleteAccount.tsx diff --git a/backend/src/modules/activity-log/repositories/activity-log.repository.ts b/backend/src/modules/activity-log/repositories/activity-log.repository.ts index d2f1b2b1a..b60bfe51f 100644 --- a/backend/src/modules/activity-log/repositories/activity-log.repository.ts +++ b/backend/src/modules/activity-log/repositories/activity-log.repository.ts @@ -265,7 +265,7 @@ export class ActivityLogRepositoryService return null; } - async deleteMany(ids: string[]): Promise { - await this.activityLogRepo.delete(ids); + async deleteManyByVolunteerId(volunteerId: string): Promise { + await this.activityLogRepo.delete({ volunteerId }); } } diff --git a/backend/src/modules/activity-log/services/activity-log.facade.ts b/backend/src/modules/activity-log/services/activity-log.facade.ts index efd84f3de..70dc47251 100644 --- a/backend/src/modules/activity-log/services/activity-log.facade.ts +++ b/backend/src/modules/activity-log/services/activity-log.facade.ts @@ -55,7 +55,7 @@ export class ActivityLogFacade { return this.activityLogRepository.delete(id); } - async deleteMany(ids: string[]): Promise { - return this.activityLogRepository.deleteMany(ids); + async deleteManyByVolunteerId(volunteerId: string): Promise { + return this.activityLogRepository.deleteManyByVolunteerId(volunteerId); } } diff --git a/backend/src/modules/documents/repositories/contract.repository.ts b/backend/src/modules/documents/repositories/contract.repository.ts index ed6f132cd..da7998a56 100644 --- a/backend/src/modules/documents/repositories/contract.repository.ts +++ b/backend/src/modules/documents/repositories/contract.repository.ts @@ -59,6 +59,7 @@ export class ContractRepositoryService const query = this.contractRepository .createQueryBuilder('contract') + .withDeleted() .leftJoinAndMapOne( 'contract.organization', 'contract.organization', @@ -187,6 +188,7 @@ export class ContractRepositoryService template: true, createdByAdmin: true, }, + withDeleted: true, }); return ContractTransformer.fromEntity(contract); diff --git a/backend/src/modules/user/exceptions/exceptions.ts b/backend/src/modules/user/exceptions/exceptions.ts index e5c5228c0..850cda71d 100644 --- a/backend/src/modules/user/exceptions/exceptions.ts +++ b/backend/src/modules/user/exceptions/exceptions.ts @@ -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; @@ -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', + }, }; diff --git a/backend/src/modules/user/repositories/regular-user.repository.ts b/backend/src/modules/user/repositories/regular-user.repository.ts index ad31f0d61..43202936d 100644 --- a/backend/src/modules/user/repositories/regular-user.repository.ts +++ b/backend/src/modules/user/repositories/regular-user.repository.ts @@ -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; } } diff --git a/backend/src/modules/volunteer/repositories/volunteer.repository.ts b/backend/src/modules/volunteer/repositories/volunteer.repository.ts index 46248e3ee..a66e81b73 100644 --- a/backend/src/modules/volunteer/repositories/volunteer.repository.ts +++ b/backend/src/modules/volunteer/repositories/volunteer.repository.ts @@ -303,7 +303,7 @@ export class VolunteerRepositoryService }); } - async deleteManyAndProfiles( + async softDeleteManyAndProfiles( userId: string, ): Promise<{ deletedProfiles: string[]; deletedVolunteers: string[] }> { const volunteerRecords = await this.volunteerRepository.find({ @@ -311,26 +311,33 @@ export class VolunteerRepositoryService 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) || [], }; } diff --git a/backend/src/modules/volunteer/services/volunteer.facade.ts b/backend/src/modules/volunteer/services/volunteer.facade.ts index af091f1bd..e315384ac 100644 --- a/backend/src/modules/volunteer/services/volunteer.facade.ts +++ b/backend/src/modules/volunteer/services/volunteer.facade.ts @@ -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); } } diff --git a/backend/src/usecases/user/delete-account.usecase.ts b/backend/src/usecases/user/delete-account.usecase.ts index f4bd84d15..f0c375dfc 100644 --- a/backend/src/usecases/user/delete-account.usecase.ts +++ b/backend/src/usecases/user/delete-account.usecase.ts @@ -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 { @@ -39,42 +40,49 @@ export class DeleteAccountRegularUserUsecase implements IUseCaseService { 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; } diff --git a/mobile/app.config.ts b/mobile/app.config.ts index 8e7015dad..3847435ea 100644 --- a/mobile/app.config.ts +++ b/mobile/app.config.ts @@ -18,7 +18,7 @@ const expoConfig: ExpoConfig = { }, assetBundlePatterns: ['**/*'], ios: { - buildNumber: '1', + buildNumber: '4', supportsTablet: true, bundleIdentifier: 'org.commitglobal.vic', entitlements: { @@ -32,7 +32,7 @@ const expoConfig: ExpoConfig = { }, }, android: { - versionCode: 1, + versionCode: 3, adaptiveIcon: { foregroundImage: './src/assets/images/adaptive-icon.png', backgroundColor: '#ffffff', @@ -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.', }, ], ], @@ -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', diff --git a/mobile/src/assets/locales/en/translation.json b/mobile/src/assets/locales/en/translation.json index 6d26c3a08..977b7c0d9 100644 --- a/mobile/src/assets/locales/en/translation.json +++ b/mobile/src/assets/locales/en/translation.json @@ -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", @@ -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." } } diff --git a/mobile/src/assets/locales/ro/translation.json b/mobile/src/assets/locales/ro/translation.json index 23bfd8c1e..a6e7e5d54 100644 --- a/mobile/src/assets/locales/ro/translation.json +++ b/mobile/src/assets/locales/ro/translation.json @@ -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", @@ -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." } } diff --git a/mobile/src/assets/svg/trash.js b/mobile/src/assets/svg/trash.js new file mode 100644 index 000000000..72acac8cc --- /dev/null +++ b/mobile/src/assets/svg/trash.js @@ -0,0 +1,3 @@ +export default ` + +`; diff --git a/mobile/src/routes/Private.tsx b/mobile/src/routes/Private.tsx index 3db604b43..c2a7b2e69 100644 --- a/mobile/src/routes/Private.tsx +++ b/mobile/src/routes/Private.tsx @@ -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(); @@ -60,6 +61,7 @@ const Private = () => ( + ); diff --git a/mobile/src/screens/DeleteAccount.tsx b/mobile/src/screens/DeleteAccount.tsx new file mode 100644 index 000000000..8802b073d --- /dev/null +++ b/mobile/src/screens/DeleteAccount.tsx @@ -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 ( + + + + {`${t('paragraph')}`} + + {!!deleteAccountError && ( + + {`${t('error', { value: Constants.expoConfig?.extra?.contactEmail })}`} + + )} + + + ); +}; + +export default DeleteAccount; diff --git a/mobile/src/screens/Settings.tsx b/mobile/src/screens/Settings.tsx index b1a9c12e4..103945ad3 100644 --- a/mobile/src/screens/Settings.tsx +++ b/mobile/src/screens/Settings.tsx @@ -3,6 +3,7 @@ import bell from '../assets/svg/bell'; import identification from '../assets/svg/identification'; import information from '../assets/svg/information'; import key from '../assets/svg/key'; +import trash from '../assets/svg/trash'; import logoutIcon from '../assets/svg/logout'; import user from '../assets/svg/user'; import PageLayout from '../layouts/PageLayout'; @@ -27,6 +28,7 @@ export enum SETTINGS_ROUTES { NOTIFICATIONS_SETTINGS = 'notifications-settings', INFORMATION = 'information', LOGOUT = 'logout', + DELETE_ACCOUNT = 'delete-account', } export const SETTING_SCREENS = [ @@ -43,6 +45,7 @@ export const SETTING_SCREENS = [ route: SETTINGS_ROUTES.NOTIFICATIONS_SETTINGS, }, { icon: information, label: i18n.t('settings:information'), route: SETTINGS_ROUTES.INFORMATION }, + { icon: trash, label: i18n.t('settings:delete'), route: SETTINGS_ROUTES.DELETE_ACCOUNT }, { icon: logoutIcon, label: i18n.t('settings:logout'), route: SETTINGS_ROUTES.LOGOUT }, ]; diff --git a/mobile/src/services/user/user.api.ts b/mobile/src/services/user/user.api.ts index ca211cbeb..a966d94f2 100644 --- a/mobile/src/services/user/user.api.ts +++ b/mobile/src/services/user/user.api.ts @@ -46,3 +46,7 @@ export const updateUserProfile = async ( headers: { 'Content-Type': 'multipart/form-data' }, }).then((res) => res.data); }; + +export const deleteAccount = async () => { + return API.delete('/mobile/user').then((res) => res.data); +}; diff --git a/mobile/src/services/user/user.service.ts b/mobile/src/services/user/user.service.ts index 7a1a9ada5..062e9b080 100644 --- a/mobile/src/services/user/user.service.ts +++ b/mobile/src/services/user/user.service.ts @@ -1,6 +1,7 @@ import { useMutation, useQuery } from 'react-query'; import { createUserProfile, + deleteAccount, getUserProfile, updateUserPersonalData, updateUserProfile, @@ -64,3 +65,7 @@ export const useUpdateUserProfileMutation = () => { { onSuccess: (data) => setUserProfile({ ...oldProfile, ...data }) }, ); }; + +export const useDeleteAccountMutation = () => { + return useMutation(['delete-account'], () => deleteAccount()); +};