Skip to content

Commit be5e579

Browse files
committed
ajout d'un mode automatique pour la synchronisation
SESAME_IDENTITY_SYNC_MODE — valeurs manual (défaut) ou auto. manuel : comportement par defaut, les identités modifiées restent à synchroniser auto : les identitées qui sont modifiées sont automatiquement modifiées le endpoint upsert
1 parent 5b20b41 commit be5e579

4 files changed

Lines changed: 92 additions & 15 deletions

File tree

apps/api/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ SESAME_REDIS_URI="redis://sesame-redis:6379/0"
99
SESAME_MONGO_URI="mongodb://sesame-mongodb:27017/sesame"
1010
# Adresse du frontal de changement de mot de passe
1111
SESAME_FRONT_MDP="http://localhost:3000"
12+
SESAME_IDENTITY_SYNC_MODE=manual

apps/api/src/config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,28 @@ export const validationSchema = Joi.object({
110110

111111
SESAME_IDENTITY_DOUBLON_SEARCH_ATTRIBUTES: Joi.string().default(''),
112112

113+
/**
114+
* Mode de synchronisation des identités après une modification.
115+
* - `manual` (défaut) : l'identité passe en TO_VALIDATE et attend une validation puis une synchronisation manuelle.
116+
* - `auto` : l'identité passe directement en TO_SYNC et la synchronisation vers les backends est déclenchée.
117+
*/
118+
SESAME_IDENTITY_SYNC_MODE: Joi.string().valid('manual', 'auto').default('manual'),
119+
113120
/**
114121
* Active trust proxy Express (1 hop) pour que req.ip / X-Forwarded-For reflètent le client derrière un reverse-proxy.
115122
* @see https://expressjs.com/en/guide/behind-proxies.html
116123
*/
117124
SESAME_TRUST_PROXY: Joi.string().valid('0', '1', 'false', 'true', 'on', 'off', '').default('0'),
118125
});
119126

127+
/**
128+
* Mode de synchronisation des identités après une modification
129+
*
130+
* @description `manual` conserve l'étape de validation avant synchronisation,
131+
* `auto` déclenche la synchronisation vers les backends dès la modification.
132+
*/
133+
export type IdentitySyncMode = 'manual' | 'auto';
134+
120135
/**
121136
* Configuration d'un plugin Mongoose
122137
*
@@ -214,6 +229,8 @@ export interface ConfigInstance {
214229
};
215230
identities: {
216231
doublonSearchAttributes: string[];
232+
/** Mode de synchronisation appliqué après la modification d'une identité. */
233+
syncMode: IdentitySyncMode;
217234
};
218235
swagger: {
219236
path: string;
@@ -355,6 +372,7 @@ export default (): ConfigInstance => ({
355372
doublonSearchAttributes: process.env['SESAME_IDENTITY_DOUBLON_SEARCH_ATTRIBUTES']
356373
? process.env['SESAME_IDENTITY_DOUBLON_SEARCH_ATTRIBUTES'].split(',').map((attr) => attr.trim())
357374
: ['additionalFields.attributes.supannPerson.supannOIDCDatedeNaissance', 'inetOrgPerson.givenName'],
375+
syncMode: /^auto$/i.test(process.env['SESAME_IDENTITY_SYNC_MODE'] || '') ? 'auto' : 'manual',
358376
},
359377
sms: {
360378
host: process.env['SESAME_SMPP_SERVER'] || '',

apps/api/src/management/identities/identities-crud.service.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Identities } from '~/management/identities/_schemas/identities.schema';
1717
import { BadRequestException, HttpException } from '@nestjs/common';
1818
import { CountOptions } from 'mongodb';
1919
import { IdentityLifecycleDefault, IdentityLifecycleState } from './_enums/lifecycle.enum';
20+
import { IdentitySyncMode } from '~/config';
2021

2122
export const COUNT_ALL_MAX_ITERATIONS = 500;
2223

@@ -70,7 +71,20 @@ export class IdentitiesCrudService extends AbstractIdentitiesService {
7071
//pour la validation le employeeNumber doit exister on en met un avec une valeur par defaut
7172
check.attributes.inetOrgPerson.employeeNumber = ['1'];
7273
const validations = await this._validation.validate(check);
74+
75+
// En mode <auto> (SESAME_IDENTITY_SYNC_MODE), l'identité est synchronisée dès sa création,
76+
// sans passer par l'étape de validation manuelle.
77+
const autoSync = this.isAutoSyncEnabled() && (data.state === undefined || data.state === IdentityState.TO_CREATE);
78+
if (autoSync) {
79+
data.state = IdentityState.TO_SYNC;
80+
}
81+
7382
const created: Document<T, any, T> = await super.create(data, options);
83+
84+
if (autoSync) {
85+
await this.triggerAutoSync(created._id);
86+
}
87+
7488
return created;
7589
}
7690

@@ -125,7 +139,9 @@ export class IdentitiesCrudService extends AbstractIdentitiesService {
125139
throw new HttpException('Uid ou mail déjà présent dans une autre identité', 400);
126140
}
127141
// if (update.state === IdentityState.TO_COMPLETE) {
128-
update = { ...update, state: IdentityState.TO_VALIDATE };
142+
// En mode <auto> (SESAME_IDENTITY_SYNC_MODE), l'identité est synchronisée sans validation manuelle.
143+
const autoSync = this.isAutoSyncEnabled();
144+
update = { ...update, state: autoSync ? IdentityState.TO_SYNC : IdentityState.TO_VALIDATE };
129145

130146
await this.checkInetOrgPersonJpegPhoto(update);
131147

@@ -135,8 +151,44 @@ export class IdentitiesCrudService extends AbstractIdentitiesService {
135151
// }
136152
//update.state = IdentityState.TO_VALIDATE;
137153
const updated = await super.update(_id, update, options);
138-
//TODO: add backends service logic here (TO_SYNC)
139-
return await this.generateFingerprint(updated as unknown as Identities);
154+
const identity = await this.generateFingerprint<T>(updated as unknown as Identities);
155+
156+
if (autoSync) {
157+
await this.triggerAutoSync(_id);
158+
}
159+
160+
return identity;
161+
}
162+
163+
/**
164+
* Indique si la synchronisation automatique après modification est activée
165+
*
166+
* @returns true si SESAME_IDENTITY_SYNC_MODE vaut <auto>, false en mode manuel (défaut)
167+
*/
168+
protected isAutoSyncEnabled(): boolean {
169+
return this.config.get<IdentitySyncMode>('identities.syncMode') === 'auto';
170+
}
171+
172+
/**
173+
* Déclenche la synchronisation vers les backends d'une identité modifiée
174+
*
175+
* @description Une erreur de synchronisation n'invalide pas la modification : l'identité reste
176+
* en état TO_SYNC et pourra être resynchronisée manuellement ou par un syncall.
177+
* @param _id - Identifiant de l'identité à synchroniser
178+
*/
179+
protected async triggerAutoSync(_id: Types.ObjectId | any): Promise<void> {
180+
try {
181+
await this.backends.syncIdentities([`${_id}`], {
182+
async: true,
183+
// L'identité bascule en PROCESSING dès la mise en file : elle n'apparaît pas
184+
// dans les <identités à synchroniser> en attente d'une action manuelle.
185+
switchToProcessing: true,
186+
comment: 'Synchronisation automatique après modification',
187+
});
188+
this.logger.log(`Auto sync triggered for identity <${_id}>`);
189+
} catch (error) {
190+
this.logger.error(`Auto sync failed for identity <${_id}>: ${error?.message}`, error?.stack);
191+
}
140192
}
141193

142194
public async updateLifecycle<T extends AbstractSchema | Document>(

apps/web/src/layouts/default.vue

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ q-layout(view="hHh LpR lff" style="margin-top: -1px;")
2020
</template>
2121

2222
<script lang="ts">
23-
import { IdentityState } from '~/constants/enums'
2423
import { useIdentityStateStore } from '~/stores/identityState'
2524
import { loadingBarDefaults } from '~/composables/useLoadingBarHijackFilter'
2625
import { attachSocketIoDebug } from '~/composables/useSocketIoDebug'
@@ -82,7 +81,8 @@ export default defineNuxtComponent({
8281
menuParts,
8382
getMenuByPart,
8483
identityStateStore,
85-
eventSeamlessTotal: identityStateStore.getStateValue(IdentityState.PROCESSING),
84+
// Le total est alimenté par l'action « tout synchroniser » ou déduit des jobs reçus via websocket.
85+
eventSeamlessTotal: 0,
8686
}
8787
},
8888
computed: {
@@ -144,9 +144,11 @@ export default defineNuxtComponent({
144144
void this.fetchDaemonUpdateInfo(payload.version)
145145
}
146146
},
147-
syncing(payload: { count: number }) {
148-
this.eventSeamlessTotal = payload.count
147+
syncing(payload: { count: number | string }) {
148+
const total = Number(payload.count)
149+
this.eventSeamlessTotal = Number.isFinite(total) ? total : 0
149150
this.eventSeamlessCurrent = 0
151+
this.eventSeamlessCurrentJobs = {}
150152
this.eventSeamless = true
151153
},
152154
connectBackendsSocket(auth: ReturnType<typeof useAuth>): void {
@@ -207,19 +209,19 @@ export default defineNuxtComponent({
207209
return
208210
}
209211
210-
if (/^job:/.test(data.channel)) {
211-
if (this.eventSeamlessTotal === 0) {
212-
await this.identityStateStore.fetchAllStateCount()
213-
this.eventSeamlessTotal = this.identityStateStore.getStateValue(IdentityState.PROCESSING)
214-
}
215-
}
216-
217212
switch (data.channel) {
218213
case 'job:added':
219214
this.eventSeamless = true
220215
if (data.payload?.jobId) {
221216
this.eventSeamlessCurrentJobs[data.payload.jobId] = data.payload
222217
}
218+
// Jobs non déclenchés par « tout synchroniser » (synchronisation automatique après
219+
// modification par exemple) : le total est déduit des jobs reçus, sinon le panneau
220+
// resterait affiché indéfiniment.
221+
this.eventSeamlessTotal = Math.max(
222+
this.eventSeamlessTotal,
223+
this.eventSeamlessCurrent + Object.keys(this.eventSeamlessCurrentJobs).length,
224+
)
223225
break
224226
225227
case 'job:failed':
@@ -229,8 +231,12 @@ export default defineNuxtComponent({
229231
}
230232
this.eventSeamlessCurrent++
231233
232-
if (this.eventSeamlessCurrent >= this.eventSeamlessTotal) {
234+
if (
235+
this.eventSeamlessCurrent >= this.eventSeamlessTotal &&
236+
Object.keys(this.eventSeamlessCurrentJobs).length === 0
237+
) {
233238
this.eventSeamlessCurrent = 0
239+
this.eventSeamlessTotal = 0
234240
this.eventSeamlessCurrentJobs = {}
235241
setTimeout(() => {
236242
this.eventSeamless = false

0 commit comments

Comments
 (0)