diff --git a/dashboard/src/components/DataMigrator.js b/dashboard/src/components/DataMigrator.js index a9abfe528..8449de9dd 100644 --- a/dashboard/src/components/DataMigrator.js +++ b/dashboard/src/components/DataMigrator.js @@ -213,6 +213,7 @@ const duplicateDecryptedData = async ({ personIdsMapped[person._id] = newPersonId; const newPerson = { ...person, + user: userIdsMapped[person.user], documents: await recryptPersonRelatedDocuments(person, person._id, newPersonId), assignedTeams: person.assignedTeams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [], organisation: nextOrganisationId, @@ -230,6 +231,7 @@ const duplicateDecryptedData = async ({ consultationIdsMapped[consultation._id] = newConsultationId; const newConsultation = { ...consultation, + user: userIdsMapped[consultation.user], documents: await recryptPersonRelatedDocuments(consultation, consultation.person, personIdsMapped[consultation.person]), person: personIdsMapped[consultation.person], teams: consultation.teams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [], @@ -248,6 +250,7 @@ const duplicateDecryptedData = async ({ treatmentIdsMapped[treatment._id] = newTreatmentId; const newTreatment = { ...treatment, + user: userIdsMapped[treatment.user], documents: await recryptPersonRelatedDocuments(treatment, treatment.person, personIdsMapped[treatment.person]), person: personIdsMapped[treatment.person], organisation: nextOrganisationId, @@ -266,6 +269,7 @@ const duplicateDecryptedData = async ({ medicalFileIdsMapped[medicalFile._id] = newMedicalFileId; const newMedicalFile = { ...medicalFile, + user: userIdsMapped[medicalFile.user], documents: await recryptPersonRelatedDocuments(medicalFile, medicalFile.person, personIdsMapped[medicalFile.person]), person: personIdsMapped[medicalFile.person], organisation: nextOrganisationId, @@ -284,6 +288,7 @@ const duplicateDecryptedData = async ({ actionIdsMapped[action._id] = newActionId; const newAction = { ...action, + user: userIdsMapped[action.user], person: personIdsMapped[action.person], teams: action.teams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [], organisation: nextOrganisationId, @@ -325,6 +330,7 @@ const duplicateDecryptedData = async ({ commentIdsMapped[comment._id] = newCommentId; const newComment = { ...comment, + user: userIdsMapped[comment.user], team: teamIdsMapped[comment.team], organisation: nextOrganisationId, _id: newCommentId, @@ -345,6 +351,7 @@ const duplicateDecryptedData = async ({ passageIdsMapped[passage._id] = newPassageId; const newPassage = { ...passage, + user: userIdsMapped[passage.user], team: teamIdsMapped[passage.team], person: personIdsMapped[passage.person], organisation: nextOrganisationId, @@ -360,6 +367,7 @@ const duplicateDecryptedData = async ({ rencontreIdsMapped[rencontre._id] = newRencontreId; const newRencontre = { ...rencontre, + user: userIdsMapped[rencontre.user], team: teamIdsMapped[rencontre.team], person: personIdsMapped[rencontre.person], organisation: nextOrganisationId, @@ -375,6 +383,7 @@ const duplicateDecryptedData = async ({ territoryIdsMapped[territory._id] = newTerritoryId; const newTerritory = { ...territory, + user: userIdsMapped[territory.user], organisation: nextOrganisationId, _id: newTerritoryId, }; @@ -389,10 +398,10 @@ const duplicateDecryptedData = async ({ territoryObservationIdsMapped[territoryObservation._id] = newTerritoryObservationId; const newTerritoryObservation = { ...territoryObservation, + user: userIdsMapped[territoryObservation.user], territory: territoryIdsMapped[territoryObservation.territory], team: teamIdsMapped[territoryObservation.team], organisation: nextOrganisationId, - observedAt: territoryObservation.deletedAt ? territoryObservation.observedAt : territoryObservation.createdAt, _id: newTerritoryObservationId, }; newObs.push(newTerritoryObservation); @@ -405,6 +414,7 @@ const duplicateDecryptedData = async ({ placeIdsMapped[place._id] = newPlaceId; const newPlace = { ...place, + user: userIdsMapped[place.user], organisation: nextOrganisationId, _id: newPlaceId, }; @@ -418,6 +428,7 @@ const duplicateDecryptedData = async ({ relPersonPlaceIdsMapped[relPersonPlace._id] = newRelPersonPlaceId; const newRelPersonPlace = { ...relPersonPlace, + user: userIdsMapped[relPersonPlace.user], person: personIdsMapped[relPersonPlace.person], place: placeIdsMapped[relPersonPlace.place], organisation: nextOrganisationId, @@ -433,6 +444,7 @@ const duplicateDecryptedData = async ({ reportIdsMapped[report._id] = newReportId; const newReport = { ...report, + user: userIdsMapped[report.user], team: teamIdsMapped[report.team], organisation: nextOrganisationId, _id: newReportId, @@ -447,20 +459,34 @@ const duplicateDecryptedData = async ({ teams: newTeams, users: newUsers, relUserTeams: newRelUserTeams, - persons: await Promise.all(newPersons.map(preparePersonForEncryption).map(encryptItem)), - consultations: await Promise.all(newConsultations.map(prepareConsultationForEncryption(organisation.consultations)).map(encryptItem)), - treatments: await Promise.all(newTreatments.map(prepareTreatmentForEncryption).map(encryptItem)), - medicalFiles: await Promise.all(newMedicalFiles.map(prepareMedicalFileForEncryption(organisation.customFieldsMedicalFile)).map(encryptItem)), - actions: await Promise.all(newActions.map(prepareActionForEncryption).map(encryptItem)), - groups: await Promise.all(newGroups.map(prepareGroupForEncryption).map(encryptItem)), - comments: await Promise.all(newComments.map(prepareCommentForEncryption).map(encryptItem)), - passages: await Promise.all(newPassages.map(preparePassageForEncryption).map(encryptItem)), - rencontres: await Promise.all(newRencontres.map(prepareRencontreForEncryption).map(encryptItem)), - territories: await Promise.all(newTerritories.map(prepareTerritoryForEncryption).map(encryptItem)), - observations: await Promise.all(newObs.map(prepareObsForEncryption(organisation.customFieldsObs)).map(encryptItem)), - places: await Promise.all(newPlaces.map(preparePlaceForEncryption).map(encryptItem)), - relsPersonPlace: await Promise.all(newRelPersonPlaces.map(prepareRelPersonPlaceForEncryption).map(encryptItem)), - reports: await Promise.all(newReports.map(prepareReportForEncryption).map(encryptItem)), + persons: await Promise.all(newPersons.map((item) => preparePersonForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + consultations: await Promise.all( + newConsultations + .map((item) => prepareConsultationForEncryption(organisation.consultations)(item, { checkRequiredFields: false })) + .map(encryptItem) + ), + treatments: await Promise.all(newTreatments.map((item) => prepareTreatmentForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + medicalFiles: await Promise.all( + newMedicalFiles + .map((item) => prepareMedicalFileForEncryption(organisation.customFieldsMedicalFile)(item, { checkRequiredFields: false })) + .map(encryptItem) + ), + actions: await Promise.all(newActions.map((item) => prepareActionForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + groups: await Promise.all(newGroups.map((item) => prepareGroupForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + comments: await Promise.all(newComments.map((item) => prepareCommentForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + passages: await Promise.all(newPassages.map((item) => preparePassageForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + rencontres: await Promise.all(newRencontres.map((item) => prepareRencontreForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + territories: await Promise.all( + newTerritories.map((item) => prepareTerritoryForEncryption(item, { checkRequiredFields: false })).map(encryptItem) + ), + observations: await Promise.all( + newObs.map((item) => prepareObsForEncryption(organisation.customFieldsObs)(item, { checkRequiredFields: false })).map(encryptItem) + ), + places: await Promise.all(newPlaces.map((item) => preparePlaceForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), + relsPersonPlace: await Promise.all( + newRelPersonPlaces.map((item) => prepareRelPersonPlaceForEncryption(item, { checkRequiredFields: false })).map(encryptItem) + ), + reports: await Promise.all(newReports.map((item) => prepareReportForEncryption(item, { checkRequiredFields: false })).map(encryptItem)), }; }; diff --git a/dashboard/src/components/duplicateOrganisation.js b/dashboard/src/components/duplicateOrganisation.js new file mode 100644 index 000000000..83bf4bbb6 --- /dev/null +++ b/dashboard/src/components/duplicateOrganisation.js @@ -0,0 +1,154 @@ +import { encryptVerificationKey } from '../services/encryption'; +import { capture } from '../services/sentry'; +import API, { getHashedOrgEncryptionKey, decryptAndEncryptItem } from '../services/api'; + +export default async function duplicate({ organisation }) { + const hashedOrgEncryptionKey = getHashedOrgEncryptionKey(); + const encryptedVerificationKey = await encryptVerificationKey(hashedOrgEncryptionKey); + + async function recrypt(path, callback = null) { + const cryptedItems = await API.get({ + skipDecrypt: true, + path, + query: { + organisation: organisation._id, + limit: String(Number.MAX_SAFE_INTEGER), + page: String(0), + after: String(0), + withDeleted: true, + }, + }); + const encryptedItems = []; + for (const item of cryptedItems.data) { + try { + const recrypted = await decryptAndEncryptItem(item, previousKey.current, hashedOrgEncryptionKey, callback); + if (recrypted) encryptedItems.push(recrypted); + } catch (e) { + capture(e); + throw new Error( + `Impossible de déchiffrer et rechiffrer l'élément suivant: ${path} ${item._id}. Notez le numéro affiché et fournissez le à l'équipe de support.` + ); + } + } + return encryptedItems; + } + + const encryptedPersons = await recrypt('/person', async (decryptedData, item) => + recryptPersonRelatedDocuments(decryptedData, item._id, previousKey.current, hashedOrgEncryptionKey) + ); + const encryptedConsultations = await recrypt('/consultation', async (decryptedData) => + recryptPersonRelatedDocuments(decryptedData, decryptedData.person, previousKey.current, hashedOrgEncryptionKey) + ); + const encryptedTreatments = await recrypt('/treatment', async (decryptedData) => + recryptPersonRelatedDocuments(decryptedData, decryptedData.person, previousKey.current, hashedOrgEncryptionKey) + ); + const encryptedMedicalFiles = await recrypt('/medical-file', async (decryptedData) => + recryptPersonRelatedDocuments(decryptedData, decryptedData.person, previousKey.current, hashedOrgEncryptionKey) + ); + const encryptedGroups = await recrypt('/group'); + const encryptedActions = await recrypt('/action'); + const encryptedComments = await recrypt('/comment'); + const encryptedPassages = await recrypt('/passage'); + const encryptedRencontres = await recrypt('/rencontre'); + const encryptedTerritories = await recrypt('/territory'); + const encryptedTerritoryObservations = await recrypt('/territory-observation'); + const encryptedPlaces = await recrypt('/place'); + const encryptedRelsPersonPlace = await recrypt('/relPersonPlace'); + const encryptedReports = await recrypt('/report'); + + const totalToEncrypt = + encryptedPersons.length + + encryptedGroups.length + + encryptedActions.length + + encryptedConsultations.length + + encryptedTreatments.length + + encryptedMedicalFiles.length + + encryptedComments.length + + encryptedPassages.length + + encryptedRencontres.length + + encryptedTerritories.length + + encryptedTerritoryObservations.length + + encryptedRelsPersonPlace.length + + encryptedPlaces.length + + encryptedReports.length; + + totalDurationOnServer.current = totalToEncrypt * 0.005; // average 5 ms in server + + const res = await API.post({ + path: '/encrypt', + body: { + persons: encryptedPersons, + groups: encryptedGroups, + actions: encryptedActions, + consultations: encryptedConsultations, + treatments: encryptedTreatments, + medicalFiles: encryptedMedicalFiles, + comments: encryptedComments, + passages: encryptedPassages, + rencontres: encryptedRencontres, + territories: encryptedTerritories, + observations: encryptedTerritoryObservations, + places: encryptedPlaces, + relsPersonPlace: encryptedRelsPersonPlace, + reports: encryptedReports, + encryptedVerificationKey, + }, + query: { + encryptionLastUpdateAt: organisation.encryptionLastUpdateAt, + encryptionEnabled: true, + changeMasterKey: true, + }, + }); + + if (res.ok) { + } +} + +const recryptDocument = async (doc, personId, { fromKey, toKey }) => { + const content = await API.download( + { + path: doc.downloadPath ?? `/person/${personId}/document/${doc.file.filename}`, + encryptedEntityKey: doc.encryptedEntityKey, + }, + fromKey + ); + const docResult = await API.upload( + { + path: `/person/${personId}/document`, + file: new File([content], doc.file.originalname, { type: doc.file.mimetype }), + }, + toKey + ); + const { data: file, encryptedEntityKey } = docResult; + return { + _id: file.filename, + name: doc.file.originalname, + encryptedEntityKey, + createdAt: doc.createdAt, + createdBy: doc.createdBy, + downloadPath: `/person/${personId}/document/${file.filename}`, + file, + }; +}; + +const recryptPersonRelatedDocuments = async (item, id, oldKey, newKey) => { + if (!item.documents || !item.documents.length) return item; + const updatedDocuments = []; + for (const doc of item.documents) { + try { + const recryptedDocument = await recryptDocument(doc, id, { fromKey: oldKey, toKey: newKey }); + updatedDocuments.push(recryptedDocument); + } catch (e) { + console.error(e); + // we need a temporary hack, for the organisations which already changed their encryption key + // but not all the documents were recrypted + // we told them to change back from `newKey` to `oldKey` to retrieve the old documents + // and then change back to `newKey` to recrypt them in the new key + // SO + // if the recryption failed, we assume the document might have been encrypted with the newKey already + // so we push it + updatedDocuments.push(doc); + } + } + return { ...item, documents: updatedDocuments }; +}; diff --git a/dashboard/src/recoil/actions.js b/dashboard/src/recoil/actions.js index 3c53449ee..5646c8cec 100644 --- a/dashboard/src/recoil/actions.js +++ b/dashboard/src/recoil/actions.js @@ -65,7 +65,7 @@ export const allowedActionFieldsInHistory = [ ]; export const prepareActionForEncryption = (action, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !action.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(action.person)) { throw new Error('Action is missing person'); diff --git a/dashboard/src/recoil/comments.js b/dashboard/src/recoil/comments.js index f04336beb..6c692f06e 100644 --- a/dashboard/src/recoil/comments.js +++ b/dashboard/src/recoil/comments.js @@ -20,7 +20,7 @@ export const commentsState = atom({ const encryptedFields = ['comment', 'person', 'action', 'group', 'team', 'user', 'date', 'urgent']; export const prepareCommentForEncryption = (comment, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !comment.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(comment.person) && !looseUuidRegex.test(comment.action)) { throw new Error('Comment is missing person or action'); diff --git a/dashboard/src/recoil/consultations.ts b/dashboard/src/recoil/consultations.ts index 8cefbde79..f6380c91b 100644 --- a/dashboard/src/recoil/consultations.ts +++ b/dashboard/src/recoil/consultations.ts @@ -65,7 +65,7 @@ export const consultationsFieldsIncludingCustomFieldsSelector = selector({ export const prepareConsultationForEncryption = (customFieldsConsultations: CustomFieldsGroup[]) => (consultation: ConsultationInstance, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !consultation.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(consultation.person)) { throw new Error('Consultation is missing person'); diff --git a/dashboard/src/recoil/medicalFiles.ts b/dashboard/src/recoil/medicalFiles.ts index c88abc5da..289576b1e 100644 --- a/dashboard/src/recoil/medicalFiles.ts +++ b/dashboard/src/recoil/medicalFiles.ts @@ -26,7 +26,7 @@ const encryptedFields = ['person', 'documents', 'comments', 'history']; export const prepareMedicalFileForEncryption = (customFieldsMedicalFile: CustomField[]) => (medicalFile: MedicalFileInstance | NewMedicalFileInstance, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !medicalFile.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(medicalFile.person)) { throw new Error('MedicalFile is missing person'); diff --git a/dashboard/src/recoil/passages.js b/dashboard/src/recoil/passages.js index 9a5a726db..3f93e5399 100644 --- a/dashboard/src/recoil/passages.js +++ b/dashboard/src/recoil/passages.js @@ -20,7 +20,7 @@ export const passagesState = atom({ const encryptedFields = ['person', 'team', 'user', 'date', 'comment']; export const preparePassageForEncryption = (passage, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !passage.deletedAt) { + if (!!checkRequiredFields) { try { // we don't check the presence of a person because passage can be anonymous if (!looseUuidRegex.test(passage.team)) { diff --git a/dashboard/src/recoil/persons.ts b/dashboard/src/recoil/persons.ts index d3262f812..3b3b6d6e7 100644 --- a/dashboard/src/recoil/persons.ts +++ b/dashboard/src/recoil/persons.ts @@ -137,7 +137,7 @@ export const usePreparePersonForEncryption = () => { const fieldsPersonsCustomizableOptions = useRecoilValue(fieldsPersonsCustomizableOptionsSelector); const personFields = useRecoilValue(personFieldsSelector) as PredefinedField[]; const preparePersonForEncryption = (person: PersonInstance, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !person.deletedAt) { + if (!!checkRequiredFields) { try { if (!person.name) { throw new Error('Person is missing name'); diff --git a/dashboard/src/recoil/places.js b/dashboard/src/recoil/places.js index 14a7e64f5..45f73106f 100644 --- a/dashboard/src/recoil/places.js +++ b/dashboard/src/recoil/places.js @@ -20,7 +20,7 @@ export const placesState = atom({ const encryptedFields = ['user', 'name']; export const preparePlaceForEncryption = (place, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !place.deletedAt) { + if (!!checkRequiredFields) { try { if (!place.name) { throw new Error('Place is missing name'); diff --git a/dashboard/src/recoil/relPersonPlace.js b/dashboard/src/recoil/relPersonPlace.js index 702b61394..4a972be4f 100644 --- a/dashboard/src/recoil/relPersonPlace.js +++ b/dashboard/src/recoil/relPersonPlace.js @@ -20,7 +20,7 @@ export const relsPersonPlaceState = atom({ const encryptedFields = ['place', 'person', 'user']; export const prepareRelPersonPlaceForEncryption = (relPersonPlace, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !relPersonPlace.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(relPersonPlace.person)) { throw new Error('RelPersonPlace is missing person'); diff --git a/dashboard/src/recoil/rencontres.js b/dashboard/src/recoil/rencontres.js index 7cd28879a..d664b0d58 100644 --- a/dashboard/src/recoil/rencontres.js +++ b/dashboard/src/recoil/rencontres.js @@ -20,7 +20,7 @@ export const rencontresState = atom({ const encryptedFields = ['person', 'team', 'user', 'date', 'comment']; export const prepareRencontreForEncryption = (rencontre, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !rencontre.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(rencontre.person)) { throw new Error('Rencontre is missing person'); diff --git a/dashboard/src/recoil/reports.js b/dashboard/src/recoil/reports.js index 6d3504493..7916e53e1 100644 --- a/dashboard/src/recoil/reports.js +++ b/dashboard/src/recoil/reports.js @@ -72,7 +72,7 @@ export const flattenedServicesSelector = selector({ const encryptedFields = ['description', 'services', 'team', 'date', 'collaborations', 'oldDateSystem']; export const prepareReportForEncryption = (report, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !report.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(report.team)) { throw new Error('Report is missing team'); diff --git a/dashboard/src/recoil/territory.js b/dashboard/src/recoil/territory.js index 590755233..8a5736250 100644 --- a/dashboard/src/recoil/territory.js +++ b/dashboard/src/recoil/territory.js @@ -20,7 +20,7 @@ export const territoriesState = atom({ const encryptedFields = ['name', 'perimeter', 'types', 'user']; export const prepareTerritoryForEncryption = (territory, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !territory.deletedAt) { + if (!!checkRequiredFields) { try { if (!territory.name) { throw new Error('Territory is missing name'); diff --git a/dashboard/src/recoil/territoryObservations.js b/dashboard/src/recoil/territoryObservations.js index 7a57ab9df..bcbc5fc05 100644 --- a/dashboard/src/recoil/territoryObservations.js +++ b/dashboard/src/recoil/territoryObservations.js @@ -92,7 +92,7 @@ const compulsoryEncryptedFields = ['territory', 'user', 'team', 'observedAt']; export const prepareObsForEncryption = (customFields) => (obs, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !obs.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(obs.territory)) { throw new Error('Observation is missing territory'); diff --git a/dashboard/src/recoil/treatments.ts b/dashboard/src/recoil/treatments.ts index e5f87930d..8a059f005 100644 --- a/dashboard/src/recoil/treatments.ts +++ b/dashboard/src/recoil/treatments.ts @@ -35,7 +35,7 @@ export const allowedTreatmentFieldsInHistory = [ ]; export const prepareTreatmentForEncryption = (treatment: TreatmentInstance, { checkRequiredFields = true } = {}) => { - if (!!checkRequiredFields && !treatment.deletedAt) { + if (!!checkRequiredFields) { try { if (!looseUuidRegex.test(treatment.person)) { throw new Error('Treatment is missing person'); diff --git a/dashboard/src/types/medicalFile.ts b/dashboard/src/types/medicalFile.ts index 22ba07d51..88a05cadf 100644 --- a/dashboard/src/types/medicalFile.ts +++ b/dashboard/src/types/medicalFile.ts @@ -13,7 +13,6 @@ export interface MedicalFileInstance extends AdditionalProps { comments: any[]; createdAt: Date; updatedAt: Date; - deletedAt?: Date; } export interface NewMedicalFileInstance extends Omit {} diff --git a/dashboard/src/types/treatment.ts b/dashboard/src/types/treatment.ts index 41ae2c0fd..2b2edc32c 100644 --- a/dashboard/src/types/treatment.ts +++ b/dashboard/src/types/treatment.ts @@ -17,6 +17,5 @@ export interface TreatmentInstance { comments: any[]; createdAt: Date; updatedAt: Date; - deletedAt?: Date; history: any[]; }