Skip to content

Commit e9a71d0

Browse files
committed
bug #107
1 parent 762de2e commit e9a71d0

4 files changed

Lines changed: 112 additions & 11 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
tasks:
2+
- name: "identities-init-invitation-expire"
3+
description: "Expiration des invitations d'initialisation de compte"
4+
enabled: true
5+
schedule: "1 * * * *" # Tous les jours à 08:00
6+
handler: "identities-init-invitation-expire"
7+
options:

apps/api/src/management/identities/_enums/init-state.enum.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export enum InitStatesEnum {
22
NOSENT = 0,
33
SENT = 1,
44
INITIALIZED = 2,
5+
OUTOFDATE = -1,
56
}

apps/api/src/management/identities/identities.command.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import { MailerService } from '@nestjs-modules/mailer';
1616
import { MailadmService } from '~/settings/mailadm.service';
1717
import { get } from 'radash';
1818
import { IdentitiesPasswordExpirationReminderService } from '~/management/identities/identities-password-expiration-reminder.service';
19+
import { InitStatesEnum } from '~/management/identities/_enums/init-state.enum';
20+
import {
21+
buildExpiredInitInvitationFilter,
22+
DEFAULT_INIT_TOKEN_TTL_SECONDS,
23+
getInitInvitationExpirationCutoff,
24+
} from '~/management/passwd/init-invitation-expiration.helper';
1925

2026
@SubCommand({ name: 'fingerprint' })
2127
export class IdentitiesFingerprintCommand extends CommandRunner {
@@ -320,6 +326,90 @@ export class IdentitiesPasswordExpirationReminderCommand extends CommandRunner {
320326
}
321327
}
322328

329+
type IdentitiesInitExpireOptions = {
330+
dryRun?: boolean;
331+
};
332+
333+
@CronConsoleHandler({
334+
handler: 'identities-init-invitation-expire',
335+
command: 'identities init expire',
336+
label: "Expiration des invitations d'initialisation de compte",
337+
arguments: [
338+
{
339+
name: 'dryRun',
340+
label: 'Simulation',
341+
description: 'Compte les invitations expirées sans modifier les identités.',
342+
type: 'boolean',
343+
default: false,
344+
},
345+
],
346+
})
347+
@SubCommand({ name: 'init' })
348+
export class IdentitiesInitExpireCommand extends CommandRunner {
349+
private readonly logger = new Logger(IdentitiesInitExpireCommand.name);
350+
351+
public constructor(protected moduleRef: ModuleRef) {
352+
super();
353+
}
354+
355+
async run(inputs: string[], options: IdentitiesInitExpireOptions): Promise<void> {
356+
const subTask = inputs?.[0];
357+
if (subTask !== 'expire') {
358+
console.error('Usage: yarn run console identities init expire [--dryRun]');
359+
return;
360+
}
361+
362+
// Services singleton : `ModuleRef.resolve()` ne fonctionne que pour les providers transient / request-scoped.
363+
const identities = this.moduleRef.get(IdentitiesCrudService, { strict: false });
364+
const passwdadm = this.moduleRef.get(PasswdadmService, { strict: false });
365+
366+
const policies = await passwdadm.getPolicies();
367+
const ttlSeconds = Number(policies?.initTokenTTL) || DEFAULT_INIT_TOKEN_TTL_SECONDS;
368+
369+
const now = new Date();
370+
const cutoff = getInitInvitationExpirationCutoff(ttlSeconds, now);
371+
const filter = buildExpiredInitInvitationFilter(ttlSeconds, now);
372+
373+
this.logger.log(
374+
`Checking outdated init invitations: initTokenTTL=${ttlSeconds}s cutoff=${cutoff.toISOString()} dryRun=${!!options?.dryRun}`,
375+
);
376+
377+
const total = await identities.model.countDocuments(filter);
378+
if (total === 0) {
379+
this.logger.log('No outdated init invitation found.');
380+
return;
381+
}
382+
383+
if (options?.dryRun) {
384+
const candidates = await identities.model.find(filter).select({ _id: 1, initInfo: 1 }).lean();
385+
for (const candidate of candidates) {
386+
this.logger.warn(
387+
`[dryRun] Identity <${candidate._id}> init invitation expired (initDate=${candidate?.initInfo?.initDate?.toISOString?.() ?? 'n/a'})`,
388+
);
389+
}
390+
this.logger.log(`[dryRun] ${total} identity(ies) would be flagged as OUTOFDATE.`);
391+
return;
392+
}
393+
394+
// Seules les identités encore en SENT sont passées en OUTOFDATE (garanti par le filtre).
395+
const result = await identities.model.updateMany(filter, {
396+
$set: { initState: InitStatesEnum.OUTOFDATE },
397+
});
398+
399+
this.logger.log(`Outdated init invitations: ${result.modifiedCount}/${total} identity(ies) set to OUTOFDATE.`);
400+
}
401+
402+
@Option({
403+
flags: '--dryRun [dryRun]',
404+
description: 'Compte les invitations expirées sans modifier les identités.',
405+
defaultValue: false,
406+
})
407+
parseDryRun(val: string): boolean {
408+
if (val === undefined || val === null || val === '') return true;
409+
return /^(1|true|on|yes)$/i.test(String(val).trim());
410+
}
411+
}
412+
323413
@Command({
324414
name: 'identities',
325415
arguments: '<task>',
@@ -328,6 +418,7 @@ export class IdentitiesPasswordExpirationReminderCommand extends CommandRunner {
328418
IdentitiesCancelFusionCommand,
329419
IdentitiesPwnedCommand,
330420
IdentitiesPasswordExpirationReminderCommand,
421+
IdentitiesInitExpireCommand,
331422
],
332423
})
333424
export class IdentitiesCommand extends CommandRunner {

apps/web/src/constants/defaultMenuEntries.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -191,14 +191,26 @@ export function getDefaultMenuEntries(): MenuItem[] {
191191
icon: 'mdi-email-fast',
192192
label: 'Invitations envoyées',
193193
name: 'invitations_envoyées',
194-
path: '/identities/table?limit=10&skip=0&filters[%23initState]=1&initInvitationExpired=false&sort[metadata.lastUpdatedAt]=desc',
194+
path: '/identities/table?limit=10&skip=0&filters[%23initState]=1&sort[metadata.lastUpdatedAt]=desc',
195195
color: 'warning',
196196
textColor: 'black',
197197
part: normalizeNameFromLabel(MenuPart.ACTIVATION),
198198
badge: { color: 'warning', textColor: 'black' },
199199
hideInMenuBar: false,
200200
acl: ['/management/identities'],
201201
},
202+
{
203+
icon: 'mdi-email-off',
204+
label: 'Invitations périmées',
205+
name: 'invitations_périmées',
206+
path: '/identities/table?limit=10&skip=0&filters[%23initState]=-1&sort[metadata.lastUpdatedAt]=desc',
207+
color: "negative",
208+
textColor: 'white',
209+
part: normalizeNameFromLabel(MenuPart.ACTIVATION),
210+
badge: { color: "negative", textColor: 'white' },
211+
hideInMenuBar: false,
212+
acl: ['/management/identities'],
213+
},
202214
{
203215
icon: 'mdi-email-open',
204216
label: 'Comptes activés',
@@ -211,16 +223,6 @@ export function getDefaultMenuEntries(): MenuItem[] {
211223
hideInMenuBar: false,
212224
acl: ['/management/identities'],
213225
},
214-
{
215-
icon: 'mdi-email-remove',
216-
label: 'Invitations périmées',
217-
name: 'invitations_périmées',
218-
path: '/identities/outdated',
219-
color: 'accent',
220-
part: normalizeNameFromLabel(MenuPart.ACTIVATION),
221-
hideInMenuBar: false,
222-
acl: ['/management/identities'],
223-
},
224226
]
225227
}
226228

0 commit comments

Comments
 (0)