Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions api/doc/organizations/post-partner-create-req/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ export default {
},
name: {
type: 'string'
},
contactEmail: {
type: 'string'
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion api/src/organizations/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
13 changes: 9 additions & 4 deletions api/src/storages/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
})
Expand All @@ -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': '' } }
)
}
}
Expand Down
4 changes: 2 additions & 2 deletions api/src/utils/partners.ts
Original file line number Diff line number Diff line change
@@ -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<Partner, 'name' | 'contactEmail'>, 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,
Expand Down
2 changes: 1 addition & 1 deletion api/types/partner/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down
6 changes: 6 additions & 0 deletions tests/features/organizations.api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 1 addition & 11 deletions ui/src/components/add-partner-superadmin-menu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,6 @@
autocomplete="off"
@update:search="onSearch"
/>
<v-text-field
v-model="contactEmail"
:label="$t('common.contactEmail')"
name="contactEmail"
density="compact"
variant="outlined"
autocomplete="off"
/>
</v-form>
</v-card-text>
<v-card-actions>
Expand Down Expand Up @@ -87,13 +79,11 @@ const emit = defineEmits(['change'])
const menu = ref(false)
const createForm = ref<InstanceType<typeof VForm>>()
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()
})
Expand All @@ -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')
Expand Down
18 changes: 18 additions & 0 deletions upgrade/8.20.1/remove-established-partner-emails.ts
Original file line number Diff line number Diff line change
@@ -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
Loading