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 5a1e3d1fe..a66e81b73 100644 --- a/backend/src/modules/volunteer/repositories/volunteer.repository.ts +++ b/backend/src/modules/volunteer/repositories/volunteer.repository.ts @@ -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/usecases/user/delete-account.usecase.ts b/backend/src/usecases/user/delete-account.usecase.ts index 8349da43e..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,41 +40,48 @@ 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.softDeleteManyAndProfiles(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 b5c76649a..3847435ea 100644 --- a/mobile/app.config.ts +++ b/mobile/app.config.ts @@ -72,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 c3653814b..977b7c0d9 100644 --- a/mobile/src/assets/locales/en/translation.json +++ b/mobile/src/assets/locales/en/translation.json @@ -744,6 +744,6 @@ "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": "Error on account 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 18d2feecc..a6e7e5d54 100644 --- a/mobile/src/assets/locales/ro/translation.json +++ b/mobile/src/assets/locales/ro/translation.json @@ -739,6 +739,6 @@ "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": "Eroare la stergerea contului" + "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/screens/DeleteAccount.tsx b/mobile/src/screens/DeleteAccount.tsx index 0859c8f36..8802b073d 100644 --- a/mobile/src/screens/DeleteAccount.tsx +++ b/mobile/src/screens/DeleteAccount.tsx @@ -7,6 +7,7 @@ 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'); @@ -48,7 +49,7 @@ const DeleteAccount = ({ navigation }: any) => { style={{ color: theme['color-danger-500'] }} category="p1" > - {`${t('error')}`} + {`${t('error', { value: Constants.expoConfig?.extra?.contactEmail })}`} )}