Skip to content

Commit

Permalink
feat: organisation truplication first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Arnaud AMBROSELLI committed Dec 5, 2023
1 parent e9dd996 commit 6cf03fb
Show file tree
Hide file tree
Showing 18 changed files with 108 additions and 51 deletions.
70 changes: 47 additions & 23 deletions api/src/controllers/migration.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
const express = require("express");
const router = express.Router();
const passport = require("passport");
const crypto = require("crypto");
const { z } = require("zod");
const { catchErrors } = require("../errors");
const validateEncryptionAndMigrations = require("../middleware/validateEncryptionAndMigrations");
const { looseUuidRegex, dateRegex } = require("../utils");
const { looseUuidRegex, isoDateRegex } = require("../utils");
const { capture } = require("../sentry");
const validateUser = require("../middleware/validateUser");
const { serializeOrganisation } = require("../utils/data-serializer");
const { Organisation, Person, Action, Comment, Report, Team, User, RelUserTeam, RelPersonPlace, sequelize, Group } = require("../db/sequelize");
const {
Organisation,
Person,
Action,
Comment,
Consultation,
Treatment,
MedicalFile,
Place,
Report,
Team,
User,
RelUserTeam,
RelPersonPlace,
Passage,
TerritoryObservation,
Territory,
Rencontre,
sequelize,
Group,
} = require("../db/sequelize");

router.put(
"/:migrationName",
Expand Down Expand Up @@ -61,6 +82,10 @@ router.put(
for (const { _id, encrypted, encryptedEntityKey } of req.body.thingsToUpdate) {
await Thing.update({ encrypted, encryptedEntityKey }, { where: { _id }, transaction: tx, paranoid: false });
}
organisation.set({
migrations: [...(organisation.migrations || []), req.params.migrationName],
migrationLastUpdateAt: new Date(),
});
}
// End of example of migration.
*/
Expand All @@ -69,9 +94,9 @@ router.put(
const encryptedItemFields = z.object({
_id: z.string().regex(looseUuidRegex),
organisation: z.string().regex(looseUuidRegex),
createdAt: z.string().regex(dateRegex),
updatedAt: z.string().regex(dateRegex),
deletedAt: z.string().regex(dateRegex).nullable(),
createdAt: z.string().regex(isoDateRegex),
updatedAt: z.string().regex(isoDateRegex),
deletedAt: z.string().regex(isoDateRegex).nullable().optional(),
encrypted: z.string(),
encryptedEntityKey: z.string(),
});
Expand All @@ -90,21 +115,11 @@ router.put(
z.object({
_id: z.string().regex(looseUuidRegex),
email: z.string().email(),
password: z.string(),
organisation: z.string().regex(looseUuidRegex),
role: z.string().min(1),
name: z.string(),
surname: z.string(),
phone: z.string(),
mobile: z.string(),
address: z.string(),
zipCode: z.string(),
city: z.string(),
country: z.string(),
comment: z.string(),
createdAt: z.string().regex(dateRegex),
updatedAt: z.string().regex(dateRegex),
deletedAt: z.string().regex(dateRegex).nullable(),
role: z.enum(["admin", "normal", "restricted-access"]),
name: z.string().optional(),
phone: z.string().optional(),
healthcareProfessional: z.boolean().optional(),
})
),
relUserTeams: z.array(
Expand All @@ -124,9 +139,9 @@ router.put(
passages: z.array(encryptedItemFields),
rencontres: z.array(encryptedItemFields),
territories: z.array(encryptedItemFields),
obs: z.array(encryptedItemFields),
observations: z.array(encryptedItemFields),
places: z.array(encryptedItemFields),
relPersonPlaces: z.array(encryptedItemFields),
relsPersonPlace: z.array(encryptedItemFields),
reports: z.array(encryptedItemFields),
});

Expand All @@ -142,7 +157,12 @@ router.put(
await Team.create(item, { transaction: tx });
}

const password = crypto.randomBytes(60).toString("hex"); // A useless password.,
const forgotPasswordResetExpires = new Date(Date.now() + 60 * 60 * 24 * 30 * 1000); // 30 days
for (let item of newOrganisation.users) {
item.forgotPasswordResetToken = crypto.randomBytes(20).toString("hex");
item.forgotPasswordResetExpires = forgotPasswordResetExpires;
item.password = password;
await User.create(item, { transaction: tx });
}

Expand Down Expand Up @@ -199,11 +219,11 @@ router.put(
}

for (let item of newOrganisation.relsPersonPlace) {
await RelPersonPlace.update(item, { transaction: tx });
await RelPersonPlace.create(item, { transaction: tx });
}

for (let item of newOrganisation.reports) {
await Report.update(item, { transaction: tx });
await Report.create(item, { transaction: tx });
}
};

Expand All @@ -212,6 +232,10 @@ router.put(
if (!organisation1 || !organisation2) return res.status(404).send({ ok: false, error: "Not Found" });
await saveOrganisation(req.body.organisation1);
await saveOrganisation(req.body.organisation2);
organisation.set({
migrations: [...(organisation.migrations || []), req.params.migrationName],
migrationLastUpdateAt: new Date(),
});
}

organisation.set({ migrating: false });
Expand Down
2 changes: 2 additions & 0 deletions api/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const positiveIntegerRegex = /^\d+$/;
const jwtRegex = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/;
const headerJwtRegex = /JWT ^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/;
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{1,3}Z$/;

const customFieldSchema = z
.object({
Expand Down Expand Up @@ -65,6 +66,7 @@ module.exports = {
jwtRegex,
headerJwtRegex,
dateRegex,
isoDateRegex,
customFieldSchema,
sanitizeAll,
};
59 changes: 44 additions & 15 deletions dashboard/src/components/DataMigrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,12 @@ const duplicateDecryptedData = async ({
};
newUsers.push(newUser);
for (const team of user.teams) {
if (!teamIdsMapped[team._id]) {
continue;
}
newRelUserTeams.push({
user: newUserId,
team: teamIdsMapped[team],
team: teamIdsMapped[team._id],
organisation: nextOrganisationId,
_id: uuidv4(),
});
Expand All @@ -203,12 +206,15 @@ const duplicateDecryptedData = async ({
const personIdsMapped = {};
const newPersons = [];
for (const person of persons) {
if (!person.name && !person.deletedAt) {
continue;
}
const newPersonId = uuidv4();
personIdsMapped[person._id] = newPersonId;
const newPerson = {
...person,
documents: await recryptPersonRelatedDocuments(person, person._id, newPersonId),
assignedTeams: person.assignedTeams.map((t) => teamIdsMapped[t]),
assignedTeams: person.assignedTeams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [],
organisation: nextOrganisationId,
_id: newPersonId,
};
Expand All @@ -217,16 +223,22 @@ const duplicateDecryptedData = async ({
const consultationIdsMapped = {};
const newConsultations = [];
for (const consultation of consultations) {
if (!consultation.person && !consultation.deletedAt) {
continue;
}
const newConsultationId = uuidv4();
consultationIdsMapped[consultation._id] = newConsultationId;
const newConsultation = {
...consultation,
documents: await recryptPersonRelatedDocuments(consultation, consultation.person, personIdsMapped[consultation.person]),
person: personIdsMapped[consultation.person],
teams: consultation.teams.map((t) => teamIdsMapped[t]),
teams: consultation.teams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [],
organisation: nextOrganisationId,
_id: newConsultationId,
};
if (!newConsultation.person && !newConsultation.deletedAt) {
continue;
}
newConsultations.push(newConsultation);
}
const treatmentIdsMapped = {};
Expand All @@ -241,6 +253,9 @@ const duplicateDecryptedData = async ({
organisation: nextOrganisationId,
_id: newTreatmentId,
};
if (!newTreatment.person && !newTreatment.deletedAt) {
continue;
}
newTreatments.push(newTreatment);
}

Expand All @@ -256,6 +271,9 @@ const duplicateDecryptedData = async ({
organisation: nextOrganisationId,
_id: newMedicalFileId,
};
if (!newMedicalFile.person && !newMedicalFile.deletedAt) {
continue;
}
newMedicalFiles.push(newMedicalFile);
}

Expand All @@ -267,10 +285,16 @@ const duplicateDecryptedData = async ({
const newAction = {
...action,
person: personIdsMapped[action.person],
teams: action.teams.map((t) => teamIdsMapped[t]),
teams: action.teams?.map((t) => teamIdsMapped[t]).filter(Boolean) ?? [],
organisation: nextOrganisationId,
_id: newActionId,
};
if (!newAction.person && !newAction.deletedAt) {
continue;
}
if (!newAction.teams?.length && !newAction.deletedAt) {
continue;
}
newActions.push(newAction);
}

Expand All @@ -280,13 +304,14 @@ const duplicateDecryptedData = async ({
const newGroupId = uuidv4();
groupIdsMapped[group._id] = newGroupId;
const newGroup = {
persons: group.persons.map((p) => personIdsMapped[p]),
relations: group.relations.map((r) => {
return {
...r,
persons: r.persons.map((p) => personIdsMapped[p]),
};
}),
persons: group.persons?.map((p) => personIdsMapped[p]),
relations:
group.relations?.map((r) => {
return {
...r,
persons: r.persons?.map((p) => personIdsMapped[p]),
};
}) ?? [],
organisation: nextOrganisationId,
_id: newGroupId,
};
Expand Down Expand Up @@ -358,6 +383,7 @@ const duplicateDecryptedData = async ({

const territoryObservationIdsMapped = {};
const newObs = [];

for (const territoryObservation of territoryObservations) {
const newTerritoryObservationId = uuidv4();
territoryObservationIdsMapped[territoryObservation._id] = newTerritoryObservationId;
Expand All @@ -366,6 +392,7 @@ const duplicateDecryptedData = async ({
territory: territoryIdsMapped[territoryObservation.territory],
team: teamIdsMapped[territoryObservation.team],
organisation: nextOrganisationId,
observedAt: territoryObservation.deletedAt ? territoryObservation.observedAt : territoryObservation.createdAt,
_id: newTerritoryObservationId,
};
newObs.push(newTerritoryObservation);
Expand Down Expand Up @@ -406,13 +433,15 @@ const duplicateDecryptedData = async ({
reportIdsMapped[report._id] = newReportId;
const newReport = {
...report,
tream: teamIdsMapped[report.team],
team: teamIdsMapped[report.team],
organisation: nextOrganisationId,
_id: newReportId,
};
newReports.push(newReport);
}

const nextPersons = await Promise.all(newPersons.map(preparePersonForEncryption).map(encryptItem));

return {
organisationId: nextOrganisationId,
teams: newTeams,
Expand All @@ -428,9 +457,9 @@ const duplicateDecryptedData = async ({
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)),
obs: await Promise.all(newObs.map(prepareObsForEncryption(organisation.customFieldsObs)).map(encryptItem)),
observations: await Promise.all(newObs.map(prepareObsForEncryption(organisation.customFieldsObs)).map(encryptItem)),
places: await Promise.all(newPlaces.map(preparePlaceForEncryption).map(encryptItem)),
relPersonPlaces: await Promise.all(newRelPersonPlaces.map(prepareRelPersonPlaceForEncryption).map(encryptItem)),
relsPersonPlace: await Promise.all(newRelPersonPlaces.map(prepareRelPersonPlaceForEncryption).map(encryptItem)),
reports: await Promise.all(newReports.map(prepareReportForEncryption).map(encryptItem)),
};
};
Expand Down Expand Up @@ -464,7 +493,7 @@ const recryptPersonRelatedDocuments = async (item, oldPersonId, newPersonId) =>
const recryptedDocument = await changeDocumentPersonId(doc, oldPersonId, newPersonId);
updatedDocuments.push(recryptedDocument);
} catch (e) {
console.error(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
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const allowedActionFieldsInHistory = [
];

export const prepareActionForEncryption = (action, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields) {
if (!!checkRequiredFields && !action.deletedAt) {
try {
if (!looseUuidRegex.test(action.person)) {
throw new Error('Action is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
if (!!checkRequiredFields && !comment.deletedAt) {
try {
if (!looseUuidRegex.test(comment.person) && !looseUuidRegex.test(comment.action)) {
throw new Error('Comment is missing person or action');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/consultations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const consultationsFieldsIncludingCustomFieldsSelector = selector({
export const prepareConsultationForEncryption =
(customFieldsConsultations: CustomFieldsGroup[]) =>
(consultation: ConsultationInstance, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields) {
if (!!checkRequiredFields && !consultation.deletedAt) {
try {
if (!looseUuidRegex.test(consultation.person)) {
throw new Error('Consultation is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/medicalFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const encryptedFields = ['person', 'documents', 'comments', 'history'];
export const prepareMedicalFileForEncryption =
(customFieldsMedicalFile: CustomField[]) =>
(medicalFile: MedicalFileInstance | NewMedicalFileInstance, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields) {
if (!!checkRequiredFields && !medicalFile.deletedAt) {
try {
if (!looseUuidRegex.test(medicalFile.person)) {
throw new Error('MedicalFile is missing person');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/passages.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const passagesState = atom({
const encryptedFields = ['person', 'team', 'user', 'date', 'comment'];

export const preparePassageForEncryption = (passage, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields) {
if (!!checkRequiredFields && !passage.deletedAt) {
try {
// we don't check the presence of a person because passage can be anonymous
if (!looseUuidRegex.test(passage.team)) {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/persons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
if (!!checkRequiredFields && !person.deletedAt) {
try {
if (!person.name) {
throw new Error('Person is missing name');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/places.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const placesState = atom({
const encryptedFields = ['user', 'name'];

export const preparePlaceForEncryption = (place, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields) {
if (!!checkRequiredFields && !place.deletedAt) {
try {
if (!place.name) {
throw new Error('Place is missing name');
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/recoil/relPersonPlace.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const relsPersonPlaceState = atom({
const encryptedFields = ['place', 'person', 'user'];

export const prepareRelPersonPlaceForEncryption = (relPersonPlace, { checkRequiredFields = true } = {}) => {
if (!!checkRequiredFields) {
if (!!checkRequiredFields && !relPersonPlace.deletedAt) {
try {
if (!looseUuidRegex.test(relPersonPlace.person)) {
throw new Error('RelPersonPlace is missing person');
Expand Down
Loading

0 comments on commit 6cf03fb

Please sign in to comment.