diff --git a/api/doc/organizations/post-partner-create-req/schema.js b/api/doc/organizations/post-partner-create-req/schema.js index 1ba0f499..a41970a8 100644 --- a/api/doc/organizations/post-partner-create-req/schema.js +++ b/api/doc/organizations/post-partner-create-req/schema.js @@ -19,9 +19,6 @@ export default { }, name: { type: 'string' - }, - contactEmail: { - type: 'string' } } } diff --git a/api/src/organizations/router.ts b/api/src/organizations/router.ts index d598d5e0..145ebc8d 100644 --- a/api/src/organizations/router.ts +++ b/api/src/organizations/router.ts @@ -513,10 +513,11 @@ if (config.managePartners) { const conflict = (orga.partners || []).find(p => p.id === partnerOrga.id) if (conflict) return res.status(400).send('cette organisation est déjà partenaire') + // this partnership is established immediately, there is no invitation workflow, so we do not + // store a contact email (unnecessary personal information) const partner: Partner = { id: partnerOrga.id, name: partnerCreate.name ?? partnerOrga.name, - contactEmail: partnerCreate.contactEmail ?? '', partnerId: nanoid(), createdAt: new Date().toISOString() } diff --git a/api/src/storages/mongo.ts b/api/src/storages/mongo.ts index 6f298463..e9f206d6 100644 --- a/api/src/storages/mongo.ts +++ b/api/src/storages/mongo.ts @@ -532,9 +532,12 @@ class MongodbStorage implements SdStorage { } async addPartner (orgId: string, partner: Partner) { - await mongo.organizations.updateOne({ _id: orgId }, { - $pull: { partners: { contactEmail: { $eq: partner.contactEmail }, id: { $exists: false } } } - }) + // remove a previous pending invitation sent to the same contact email (re-invitation) + if (partner.contactEmail) { + await mongo.organizations.updateOne({ _id: orgId }, { + $pull: { partners: { contactEmail: { $eq: partner.contactEmail }, id: { $exists: false } } } + }) + } await mongo.organizations.updateOne({ _id: orgId }, { $push: { partners: partner } }) @@ -545,9 +548,11 @@ class MongodbStorage implements SdStorage { } async validatePartner (orgId: string, partnerId: string, partner: Organization) { + // the invitation workflow is finished, the contact email is no longer needed and should + // not be kept as unnecessary personal information await mongo.organizations.updateOne( { _id: orgId, 'partners.partnerId': partnerId }, - { $set: { 'partners.$.name': partner.name, 'partners.$.id': partner.id } } + { $set: { 'partners.$.name': partner.name, 'partners.$.id': partner.id }, $unset: { 'partners.$.contactEmail': '' } } ) } } diff --git a/api/src/utils/partners.ts b/api/src/utils/partners.ts index 41d445ae..251e92c6 100644 --- a/api/src/utils/partners.ts +++ b/api/src/utils/partners.ts @@ -1,6 +1,6 @@ -import { type Organization, type Partner, type ShortenedPartnerInvitation } from '#types' +import { type Organization, type ShortenedPartnerInvitation } from '#types' -export const shortenPartnerInvitation = (partner: Pick, org: Organization, partnerId: string): ShortenedPartnerInvitation => { +export const shortenPartnerInvitation = (partner: { name: string, contactEmail: string }, org: Organization, partnerId: string): ShortenedPartnerInvitation => { return { o: org.id, on: org.name, diff --git a/api/types/partner/schema.js b/api/types/partner/schema.js index 491deb55..c74fc568 100644 --- a/api/types/partner/schema.js +++ b/api/types/partner/schema.js @@ -4,7 +4,7 @@ export default { title: 'Partner', type: 'object', additionalProperties: false, - required: ['name', 'contactEmail', 'partnerId', 'createdAt'], + required: ['name', 'partnerId', 'createdAt'], properties: { id: { type: 'string' }, name: { type: 'string' }, diff --git a/tests/features/organizations.api.spec.ts b/tests/features/organizations.api.spec.ts index 51d78407..94f3bd55 100644 --- a/tests/features/organizations.api.spec.ts +++ b/tests/features/organizations.api.spec.ts @@ -57,6 +57,8 @@ test.describe('organizations api', () => { assert.equal(orgInfo.partners.length, 1) assert.ok(orgInfo.partners[0].partnerId) assert.ok(!orgInfo.partners[0].id) + // the contact email is still stored while the invitation is pending + assert.equal(orgInfo.partners[0].contactEmail, 'test-partners2@test.com') const { ax: ax2 } = await createUser('test-partners2@test.com') const org2 = (await ax2.post('/api/organizations', { name: 'Org 2' })).data @@ -68,6 +70,8 @@ test.describe('organizations api', () => { assert.equal(orgInfo.partners.length, 1) assert.equal(orgInfo.partners[0].id, org2.id) assert.ok(orgInfo.partners[0].partnerId) + // once the invitation workflow is finished, the contact email is no longer stored + assert.ok(!orgInfo.partners[0].contactEmail) const userPartners = (await axOrg2.get(`/api/organizations/${org.id}/partners/_user-partners`)).data assert.equal(userPartners.length, 1) @@ -104,6 +108,8 @@ test.describe('organizations api', () => { assert.equal(orgInfo.partners[0].id, orgB.id) assert.equal(orgInfo.partners[0].name, 'Org B') assert.ok(orgInfo.partners[0].partnerId) + // an established partnership does not store a contact email + assert.ok(!orgInfo.partners[0].contactEmail) // creating the same partnership again is rejected await assert.rejects( diff --git a/ui/src/components/add-partner-superadmin-menu.vue b/ui/src/components/add-partner-superadmin-menu.vue index 821cc784..0e3259ed 100644 --- a/ui/src/components/add-partner-superadmin-menu.vue +++ b/ui/src/components/add-partner-superadmin-menu.vue @@ -43,14 +43,6 @@ autocomplete="off" @update:search="onSearch" /> - @@ -87,13 +79,11 @@ const emit = defineEmits(['change']) const menu = ref(false) const createForm = ref>() const selectedOrg = ref<{ id: string, name: string } | null>(null) -const contactEmail = ref('') const orgItems = ref<{ id: string, name: string }[]>([]) watch(menu, () => { if (!menu.value) return selectedOrg.value = null - contactEmail.value = '' orgItems.value = [] createForm.value?.reset() }) @@ -113,7 +103,7 @@ const confirmCreate = useAsyncAction(async () => { menu.value = false await $fetch(`organizations/${orga.id}/partners/_create`, { method: 'POST', - body: { id: selectedOrg.value.id, contactEmail: contactEmail.value || undefined } + body: { id: selectedOrg.value.id } }) sendUiNotif({ type: 'success', msg: t('common.modificationOk') }) emit('change') diff --git a/upgrade/8.20.1/remove-established-partner-emails.ts b/upgrade/8.20.1/remove-established-partner-emails.ts new file mode 100644 index 00000000..80fe9777 --- /dev/null +++ b/upgrade/8.20.1/remove-established-partner-emails.ts @@ -0,0 +1,18 @@ +import type { UpgradeScript } from '@data-fair/lib-node/upgrade-scripts.js' + +// The partner contact email is only useful during the invitation workflow. Once a partnership is +// established (the partner has an "id"), the email is unnecessary personal information that might +// become deprecated over time, so we remove it. Pending invitations (no "id" yet) keep their email. +const upgradeScript: UpgradeScript = { + description: 'Remove the no longer needed contact email from established partnerships', + async exec (db, debug) { + const res = await db.collection('organizations').updateMany( + { partners: { $elemMatch: { id: { $exists: true }, contactEmail: { $exists: true } } } }, + { $unset: { 'partners.$[partner].contactEmail': '' } }, + { arrayFilters: [{ 'partner.id': { $exists: true }, 'partner.contactEmail': { $exists: true } }] } + ) + debug(`Removed contact email from established partnerships in ${res.modifiedCount} organizations`) + } +} + +export default upgradeScript