Skip to content

Commit e248887

Browse files
committed
feat: add lifecycle history page for identities
- Implemented a new lifecycle history page for identities with timeline view and grouping options. - Added functionality to load lifecycles based on selected grouping (day, month, year). feat: create trash view for identities - Developed a trash view for identities allowing users to view and manage deleted identities. - Integrated identity details and save functionality within the trash view. feat: add job details page - Introduced a job details page with timeline representation of jobs. - Implemented grouping options for jobs and detailed views for job parameters and results. feat: create agent settings pages - Added agent settings pages including index, debug, and details views. - Implemented save and delete functionalities for agents with validation handling. feat: implement initial hash capture and restoration - Created plugins to capture and restore the initial URL hash for better navigation handling. - Enhanced scroll behavior to manage hash-based navigation effectively. chore: add HTTP type definitions - Introduced TypeScript definitions for HTTP instance to improve type safety across the application. chore: add release notes generation script - Developed a Python script to generate release notes from Git commit history. - The script supports specifying previous and target versions for changelog generation.
1 parent 516e337 commit e248887

File tree

182 files changed

+13576
-7863
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

182 files changed

+13576
-7863
lines changed

apps/api/src/config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ export const validationSchema = Joi.object({
157157
SESAME_CRON_LOG_DIRECTORY: Joi
158158
.string()
159159
.default(path.join(process.cwd(), 'logs', 'handlers')),
160+
161+
SESAME_IDENTITY_DOUBLON_SEARCH_ATTRIBUTES: Joi
162+
.string()
163+
.default(''),
160164
});
161165

162166
/**
@@ -249,6 +253,9 @@ export interface ConfigInstance {
249253
lifecycle: {
250254
triggerCronExpression: string
251255
}
256+
identities: {
257+
doublonSearchAttributes: string[]
258+
}
252259
swagger: {
253260
path: string
254261
api: string
@@ -382,6 +389,14 @@ export default (): ConfigInstance => ({
382389
lifecycle: {
383390
triggerCronExpression: process.env['SESAME_LIFECYCLE_TRIGGER_CRON'] || '*/5 * * * *',
384391
},
392+
identities: {
393+
doublonSearchAttributes: process.env['SESAME_IDENTITY_DOUBLON_SEARCH_ATTRIBUTES']
394+
? process.env['SESAME_IDENTITY_DOUBLON_SEARCH_ATTRIBUTES'].split(',').map(attr => attr.trim())
395+
: [
396+
'additionalFields.attributes.supannPerson.supannOIDCDatedeNaissance',
397+
'inetOrgPerson.givenName',
398+
],
399+
},
385400
sms: {
386401
host: process.env['SESAME_SMPP_SERVER'] || '',
387402
systemId: process.env['SESAME_SMPP_SYSTEMID'] || '',

apps/api/src/core/cron/_dto/config-task.dto.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
11
import { ApiProperty } from '@nestjs/swagger'
22
import { Type } from 'class-transformer'
3-
import { IsArray, IsBoolean, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator'
3+
import {
4+
IsArray,
5+
IsBoolean,
6+
IsNotEmpty,
7+
IsOptional,
8+
IsString,
9+
Validate,
10+
ValidateNested,
11+
ValidationArguments,
12+
ValidatorConstraint,
13+
ValidatorConstraintInterface,
14+
} from 'class-validator'
15+
16+
@ValidatorConstraint({ name: 'isStringArrayOrObject', async: false })
17+
class IsStringArrayOrObject implements ValidatorConstraintInterface {
18+
public validate(value: any, args: ValidationArguments) {
19+
if (!value) return true // Optional field
20+
21+
// Vérifie si c'est un tableau de strings
22+
if (Array.isArray(value)) {
23+
return value.every((item) => typeof item === 'string')
24+
}
25+
26+
// Vérifie si c'est un objet (mais pas un tableau ou null)
27+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
28+
return true
29+
}
30+
31+
return false
32+
}
33+
34+
defaultMessage(args: ValidationArguments) {
35+
return 'options doit être soit un tableau de strings, soit un objet clé-valeur'
36+
}
37+
}
438

539
/**
640
* DTO représentant la configuration d'une tâche cron
@@ -58,15 +92,28 @@ export class CronTaskDTO {
5892
})
5993
handler: string
6094

61-
@IsObject()
95+
/**
96+
* Options de la tâche : soit un tableau de strings (args), soit un objet clé-valeur
97+
*
98+
* @type {string[] | Record<string, any>}
99+
* @description Permet de spécifier des options supplémentaires pour la tâche cron.
100+
* Peut être un tableau de chaînes de caractères représentant des arguments,
101+
* ou un objet avec des paires clé-valeur pour des configurations plus complexes.
102+
*/
62103
@IsOptional()
104+
@Validate(IsStringArrayOrObject)
63105
@ApiProperty({
64-
type: 'object',
65-
description: 'Options spécifiques de la tâche (clé/valeur)',
66-
example: { retentionPeriodDays: 30 },
67-
additionalProperties: true,
106+
oneOf: [
107+
{ type: 'array', items: { type: 'string' } },
108+
{ type: 'object', additionalProperties: true },
109+
],
110+
description: 'Options spécifiques de la tâche : tableau de strings ou objet clé/valeur',
111+
examples: [
112+
['arg1', 'arg2'],
113+
{ key1: 'value1', key2: 'value2', retentionPeriodDays: 30 },
114+
],
68115
})
69-
options?: Record<string, any>
116+
options?: string[] | Record<string, any>
70117
}
71118

72119
export class ConfigTaskDTO {
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsBoolean, IsString } from 'class-validator';
2+
import { IsIn, IsMongoId } from 'class-validator';
3+
import { DataStatusEnum } from '../../_enums/data-status';
34

45
export class ActivationDto {
5-
@IsString()
6+
@IsMongoId()
67
@ApiProperty({ example: '66d80ab41821baca9bf965b2', description: 'Id of identity', type: String })
7-
public id: string;
8+
public id: string
89

9-
@IsBoolean()
10-
@ApiProperty({ example: 'true', description: 'true or false to enable or disable the identity', type: String })
11-
public status: boolean;
10+
@IsIn([DataStatusEnum.ACTIVE, DataStatusEnum.INACTIVE], {
11+
message: 'Le statut doit être ACTIVE ou INACTIVE.'
12+
})
13+
@ApiProperty({
14+
example: DataStatusEnum.ACTIVE,
15+
description: 'Desired status of the identity: ACTIVE or INACTIVE',
16+
enum: [DataStatusEnum.ACTIVE, DataStatusEnum.INACTIVE],
17+
type: Number,
18+
})
19+
public status: DataStatusEnum
1220
}

apps/api/src/management/identities/_schemas/identities.schema.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export class Identities extends AbstractSchema {
3232
@Prop({ type: Boolean, default: false })
3333
public ignoreLifecycle: boolean;
3434

35+
@Prop({ type: [Types.ObjectId], default: [] })
36+
public ignoreFusion: Types.ObjectId[];
37+
3538
@Prop({ type: inetOrgPersonSchema, required: true })
3639
public inetOrgPerson: inetOrgPerson;
3740

@@ -93,4 +96,4 @@ export const IdentitiesSchema = SchemaFactory.createForClass(Identities)
9396
.index(
9497
{ 'inetOrgPerson.employeeNumber': 1, 'inetOrgPerson.employeeType': 1 },
9598
{ unique: true },
96-
);
99+
)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { DataStatusEnum } from "~/management/identities/_enums/data-status";
1818
import { JobState } from "~/core/jobs/_enums/state.enum";
1919
import { inetOrgPersonDto } from './_dto/_parts/inetOrgPerson.dto';
2020
import { EventEmitter2 } from '@nestjs/event-emitter';
21+
import { ConfigService } from '@nestjs/config';
2122

2223
/**
2324
* Service abstrait pour la gestion des identités
@@ -31,6 +32,7 @@ export abstract class AbstractIdentitiesService extends AbstractServiceSchema {
3132
*
3233
* @param _model - Modèle Mongoose pour les identités
3334
* @param _validation - Service de validation des identités
35+
* @param config - Service de configuration
3436
* @param storage - Service de stockage de fichiers
3537
* @param passwdAdmService - Service d'administration des mots de passe
3638
* @param eventEmitter - Émetteur d'événements
@@ -40,6 +42,7 @@ export abstract class AbstractIdentitiesService extends AbstractServiceSchema {
4042
@InjectModel(Identities.name) protected _model: Model<Identities>,
4143
protected readonly _validation: IdentitiesValidationService,
4244
protected readonly storage: FactorydriveService,
45+
protected readonly config: ConfigService,
4346
protected readonly passwdAdmService: PasswdadmService,
4447
protected readonly eventEmitter: EventEmitter2,
4548
@Inject(forwardRef(() => BackendsService)) protected readonly backends: BackendsService,

apps/api/src/management/identities/identities-activation.controller.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,11 @@ export class IdentitiesActivationController extends AbstractController {
4444
@Body() body: ActivationDto,
4545
): Promise<Response> {
4646
try {
47-
// Validation des paramètres d'entrée
48-
if (!body?.id || typeof body.id !== 'string') {
49-
return res.status(HttpStatus.BAD_REQUEST).json({
50-
statusCode: HttpStatus.BAD_REQUEST,
51-
message: 'Valid identity ID is required',
52-
});
53-
}
54-
55-
if (typeof body.status !== 'boolean') {
56-
return res.status(HttpStatus.BAD_REQUEST).json({
57-
statusCode: HttpStatus.BAD_REQUEST,
58-
message: 'Status must be a boolean value',
59-
});
60-
}
61-
62-
// Détermination du statut cible selon le paramètre booléen
63-
const targetStatus = body.status ? DataStatusEnum.ACTIVE : DataStatusEnum.INACTIVE;
64-
65-
// Appel du service d'activation
66-
const result = await this._service.activation(body.id, targetStatus);
47+
const result = await this._service.activation(body.id, body.status)
6748

6849
return res.status(HttpStatus.OK).json({
6950
statusCode: HttpStatus.OK,
70-
message: `Identity ${body.status ? 'activated' : 'deactivated'} successfully`,
51+
message: `Identity ${body.status === DataStatusEnum.ACTIVE ? 'activated' : 'deactivated'} successfully`,
7152
data: result,
7253
});
7354

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

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Param,
99
Patch,
1010
Post,
11+
Query,
1112
Res,
1213
} from '@nestjs/common';
1314
import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
@@ -36,6 +37,8 @@ import { IdentitiesValidationService } from './validations/identities.validation
3637
import { FilestorageService } from '~/core/filestorage/filestorage.service';
3738
import { TransformersFilestorageService } from '~/core/filestorage/_services/transformers-filestorage.service';
3839
import { IdentitiesCrudService } from '~/management/identities/identities-crud.service';
40+
import { Public } from '~/_common/decorators/public.decorator';
41+
import { isEmpty } from 'radash';
3942

4043
@ApiTags('management/identities')
4144
@Controller('identities')
@@ -58,6 +61,14 @@ export class IdentitiesCrudController extends AbstractController {
5861
lifecycle: 1,
5962
};
6063

64+
protected static readonly searchFields: PartialProjectionType<any> = {
65+
'inetOrgPerson.cn': 1,
66+
'inetOrgPerson.givenName': 1,
67+
'inetOrgPerson.sn': 1,
68+
'inetOrgPerson.mail': 1,
69+
'inetOrgPerson.employeeType': 1,
70+
};
71+
6172
@Post()
6273
@ApiCreateDecorator(IdentitiesCreateDto, IdentitiesDto)
6374
public async create(
@@ -109,6 +120,7 @@ export class IdentitiesCrudController extends AbstractController {
109120
@ApiPaginatedDecorator(PickProjectionHelper(IdentitiesDto, IdentitiesCrudController.projection))
110121
public async getdeleted(
111122
@Res() res: Response,
123+
@Query('search') search: string,
112124
@SearchFilterOptions() searchFilterOptions: FilterOptions,
113125
): Promise<
114126
Response<
@@ -134,30 +146,40 @@ export class IdentitiesCrudController extends AbstractController {
134146
@ApiPaginatedDecorator(PickProjectionHelper(IdentitiesDto, IdentitiesCrudController.projection))
135147
public async search(
136148
@Res() res: Response,
149+
@Query('search') search: string,
137150
@SearchFilterSchema() searchFilterSchema: FilterSchema,
138-
@SearchFilterOptions() searchFilterOptions: FilterOptions,
139-
): Promise<
140-
Response<
141-
{
142-
statusCode: number;
143-
data?: Document<Identities, any, Identities>;
144-
total?: number;
145-
message?: string;
146-
validations?: MixedValue;
147-
},
148-
any
149-
>
150-
> {
151+
@SearchFilterOptions({ allowUnlimited: true }) searchFilterOptions: FilterOptions,
152+
): Promise<Response<{
153+
statusCode: number;
154+
data?: Document<Identities, any, Identities>;
155+
total?: number;
156+
message?: string;
157+
validations?: MixedValue;
158+
}>> {
159+
const searchFilter = {}
160+
161+
if (search && search.trim().length > 0) {
162+
const searchRequest = {}
163+
searchRequest['$or'] = Object.keys(IdentitiesCrudController.searchFields).map((key) => {
164+
return { [key]: { $regex: `^${search}`, $options: 'i' } }
165+
}).filter(item => item !== undefined)
166+
searchFilter['$and'] = [searchRequest]
167+
searchFilter['$and'].push(searchFilterSchema)
168+
} else {
169+
Object.assign(searchFilter, searchFilterSchema)
170+
}
171+
151172
const [data, total] = await this._service.findAndCount(
152-
searchFilterSchema,
173+
searchFilter,
153174
IdentitiesCrudController.projection,
154175
searchFilterOptions,
155-
);
176+
)
177+
156178
return res.status(HttpStatus.OK).json({
157179
statusCode: HttpStatus.OK,
158180
total,
159181
data,
160-
});
182+
})
161183
}
162184

163185
@Get(':_id([0-9a-fA-F]{24})')

apps/api/src/management/identities/identities-doublon.controller.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Body, Controller, Get, HttpStatus, Post, Res } from '@nestjs/common';
2-
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
1+
import { BadRequestException, Body, Controller, Get, HttpStatus, Param, Post, Query, Res } from '@nestjs/common';
2+
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
33
import { Response } from 'express';
44
import { AbstractController } from '~/_common/abstracts/abstract.controller';
55
import { PartialProjectionType } from '~/_common/types/partial-projection.type';
66
import { IdentitiesDto } from './_dto/identities.dto';
77
import { FusionDto } from '~/management/identities/_dto/fusion.dto';
88
import { IdentitiesDoublonService } from '~/management/identities/identities-doublon.service';
9+
import { Types } from 'mongoose';
910

1011
@ApiTags('management/identities')
1112
@Controller('identities')
@@ -24,16 +25,68 @@ export class IdentitiesDoublonController extends AbstractController {
2425

2526
@Get('duplicates')
2627
@ApiOperation({ summary: 'Renvoie la liste des doublons supposés' })
27-
public async getDoublons(@Res() res: Response): Promise<Response> {
28-
const data = await this._service.searchDoubles();
28+
public async getDoublons(@Res() res: Response, @Query('includeIgnored') includeIgnored?: string): Promise<Response> {
29+
const includeIgnoredBool = /^(true|1|yes|on)$/i.test(includeIgnored || '');
30+
const data = await this._service.searchDoubles(includeIgnoredBool);
2931
const total = data.length;
32+
3033
return res.status(HttpStatus.OK).json({
3134
statusCode: HttpStatus.OK,
3235
data,
3336
total,
3437
});
3538
}
3639

40+
@Post('ignore-fusion')
41+
@ApiOperation({ summary: 'Ignore la fusion pour une identité' })
42+
@ApiParam({ name: 'id', description: 'ID de l\'identité', type: String })
43+
@ApiResponse({ status: HttpStatus.OK, description: 'Fusion ignorée avec succès' })
44+
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Identité non trouvée' })
45+
public async ignoreFusionForIdentities(
46+
@Res() res: Response,
47+
@Body('ids') ids: string[],
48+
): Promise<Response> {
49+
if (ids.length !== 2) {
50+
throw new BadRequestException('Deux IDs doivent être fournis pour ignorer la fusion.');
51+
}
52+
53+
if (!ids.every(id => Types.ObjectId.isValid(id))) {
54+
throw new BadRequestException('Tous les IDs doivent être des ObjectId valides.');
55+
}
56+
57+
const data = await this._service.ignoreFusionForIdentities(ids.map(id => new Types.ObjectId(id)));
58+
59+
return res.status(HttpStatus.OK).json({
60+
statusCode: HttpStatus.OK,
61+
data,
62+
});
63+
}
64+
65+
@Post('unignore-fusion')
66+
@ApiOperation({ summary: 'Annule l\'ignorance de la fusion pour une identité' })
67+
@ApiParam({ name: 'id', description: 'ID de l\'identité', type: String })
68+
@ApiResponse({ status: HttpStatus.OK, description: 'Ignorance de fusion annulée avec succès' })
69+
@ApiResponse({ status: HttpStatus.BAD_REQUEST, description: 'Identité non trouvée' })
70+
public async unignoreFusionForIdentities(
71+
@Res() res: Response,
72+
@Body('ids') ids: string[],
73+
): Promise<Response> {
74+
if (ids.length !== 2) {
75+
throw new BadRequestException('Deux IDs doivent être fournis pour annuler l\'ignorance de la fusion.');
76+
}
77+
78+
if (!ids.every(id => Types.ObjectId.isValid(id))) {
79+
throw new BadRequestException('Tous les IDs doivent être des ObjectId valides.');
80+
}
81+
82+
const data = await this._service.unignoreFusionForIdentities(ids.map(id => new Types.ObjectId(id)));
83+
84+
return res.status(HttpStatus.OK).json({
85+
statusCode: HttpStatus.OK,
86+
data,
87+
});
88+
}
89+
3790
@Post('fusion')
3891
@ApiOperation({ summary: 'fusionne les deux identités' })
3992
@ApiResponse({ status: HttpStatus.OK })

0 commit comments

Comments
 (0)