fix(#131): co_parent en réponse parents + masquage secrets user
Contrat API fiche parent : co_parent peuplé (déjà chargé), sans password ni tokens sur user/co_parent. Doc tmp front→back. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { sanitizeUserForApi } from './sanitize-user-for-api';
|
||||
import { RoleType, StatutUtilisateurType, Users } from '../../entities/users.entity';
|
||||
|
||||
describe('sanitizeUserForApi', () => {
|
||||
const base: Users = {
|
||||
id: 'u1',
|
||||
email: 'a@b.fr',
|
||||
prenom: 'Paul',
|
||||
nom: 'Parent',
|
||||
role: RoleType.PARENT,
|
||||
statut: StatutUtilisateurType.ACTIF,
|
||||
password: 'hash',
|
||||
token_creation_mdp: 'tok',
|
||||
token_creation_mdp_expire_le: new Date(),
|
||||
password_reset_token: 'rst',
|
||||
password_reset_expires: new Date(),
|
||||
} as Users;
|
||||
|
||||
it('retire password et tokens', () => {
|
||||
const out = sanitizeUserForApi(base)!;
|
||||
expect(out.prenom).toBe('Paul');
|
||||
expect(out.nom).toBe('Parent');
|
||||
expect(out.password).toBeUndefined();
|
||||
expect(out.token_creation_mdp).toBeUndefined();
|
||||
expect(out.password_reset_token).toBeUndefined();
|
||||
});
|
||||
|
||||
it('retourne undefined si user absent', () => {
|
||||
expect(sanitizeUserForApi(null)).toBeUndefined();
|
||||
expect(sanitizeUserForApi(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Users } from 'src/entities/users.entity';
|
||||
|
||||
/** Champs sensibles exclus des réponses API (ticket #131 — user / co_parent). */
|
||||
const SENSITIVE_USER_KEYS: (keyof Users)[] = [
|
||||
'password',
|
||||
'token_creation_mdp',
|
||||
'token_creation_mdp_expire_le',
|
||||
'password_reset_token',
|
||||
'password_reset_expires',
|
||||
];
|
||||
|
||||
/**
|
||||
* Retourne une copie utilisateur sans secrets (hash MDP, tokens).
|
||||
* Utilisé pour `user` et `co_parent` dans les réponses Parents.
|
||||
*/
|
||||
export function sanitizeUserForApi(user?: Users | null): Users | undefined {
|
||||
if (!user) return undefined;
|
||||
const safe = { ...user } as Users;
|
||||
for (const key of SENSITIVE_USER_KEYS) {
|
||||
delete safe[key];
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||
import { User } from 'src/common/decorators/user.decorator';
|
||||
import { PendingFamilyDto } from './dto/pending-family.dto';
|
||||
import { DossierFamilleCompletDto } from './dto/dossier-famille-complet.dto';
|
||||
import { mapParentForApi, mapParentsForApi } from './parents.mapper';
|
||||
|
||||
@ApiTags('Parents')
|
||||
@Controller('parents')
|
||||
@@ -82,20 +83,23 @@ export class ParentsController {
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Liste des parents (user, co_parent, parentChildren) — ticket #131' })
|
||||
@ApiResponse({ status: 200, type: [Parents], description: 'Liste des parents' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
getAll(): Promise<Parents[]> {
|
||||
return this.parentsService.findAll();
|
||||
async getAll(): Promise<Parents[]> {
|
||||
const parents = await this.parentsService.findAll();
|
||||
return mapParentsForApi(parents);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Détail parent par user_id (inclut co_parent si id_co_parent renseigné) — ticket #131' })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Détails du parent par ID utilisateur' })
|
||||
@ApiResponse({ status: 404, description: 'Parent non trouvé' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
getOne(@Param('id') user_id: string): Promise<Parents> {
|
||||
return this.parentsService.findOne(user_id);
|
||||
async getOne(@Param('id') user_id: string): Promise<Parents> {
|
||||
const parent = await this.parentsService.findOne(user_id);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@@ -103,8 +107,9 @@ export class ParentsController {
|
||||
@ApiBody({ type: CreateParentDto })
|
||||
@ApiResponse({ status: 201, type: Parents, description: 'Parent créé avec succès' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
create(@Body() dto: CreateParentDto): Promise<Parents> {
|
||||
return this.parentsService.create(dto);
|
||||
async create(@Body() dto: CreateParentDto): Promise<Parents> {
|
||||
const parent = await this.parentsService.create(dto);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@@ -113,11 +118,12 @@ export class ParentsController {
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur du parent" })
|
||||
@ApiBody({ type: UpdateParentFicheAdminDto })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Fiche parent mise à jour' })
|
||||
updateFicheAdmin(
|
||||
async updateFicheAdmin(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateParentFicheAdminDto,
|
||||
): Promise<Parents> {
|
||||
return this.parentsService.updateFicheAdmin(id, dto);
|
||||
const parent = await this.parentsService.updateFicheAdmin(id, dto);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@@ -126,11 +132,12 @@ export class ParentsController {
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur du parent" })
|
||||
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Parent avec enfants mis à jour' })
|
||||
attachEnfant(
|
||||
async attachEnfant(
|
||||
@Param('id') id: string,
|
||||
@Param('enfantId') enfantId: string,
|
||||
): Promise<Parents> {
|
||||
return this.parentsService.attachEnfant(id, enfantId);
|
||||
const parent = await this.parentsService.attachEnfant(id, enfantId);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE, RoleType.ADMINISTRATEUR)
|
||||
@@ -139,11 +146,12 @@ export class ParentsController {
|
||||
@ApiParam({ name: 'id', description: "UUID utilisateur du parent" })
|
||||
@ApiParam({ name: 'enfantId', description: "UUID de l'enfant" })
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Parent avec enfants mis à jour' })
|
||||
detachEnfant(
|
||||
async detachEnfant(
|
||||
@Param('id') id: string,
|
||||
@Param('enfantId') enfantId: string,
|
||||
): Promise<Parents> {
|
||||
return this.parentsService.detachEnfant(id, enfantId);
|
||||
const parent = await this.parentsService.detachEnfant(id, enfantId);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.GESTIONNAIRE)
|
||||
@@ -152,7 +160,8 @@ export class ParentsController {
|
||||
@ApiResponse({ status: 200, type: Parents, description: 'Parent mis à jour avec succès' })
|
||||
@ApiResponse({ status: 404, description: 'Parent introuvable' })
|
||||
@ApiResponse({ status: 403, description: 'Accès refusé !' })
|
||||
update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
|
||||
return this.parentsService.update(id, dto);
|
||||
async update(@Param('id') id: string, @Body() dto: UpdateParentsDto): Promise<Parents> {
|
||||
const parent = await this.parentsService.update(id, dto);
|
||||
return mapParentForApi(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { mapParentForApi } from './parents.mapper';
|
||||
import { Parents } from '../../entities/parents.entity';
|
||||
import { RoleType, StatutUtilisateurType, Users } from '../../entities/users.entity';
|
||||
|
||||
describe('mapParentForApi', () => {
|
||||
it('expose co_parent avec prenom/nom sans secrets', () => {
|
||||
const coParent = {
|
||||
id: 'cp1',
|
||||
email: 'co@b.fr',
|
||||
prenom: 'Clara',
|
||||
nom: 'Co',
|
||||
role: RoleType.PARENT,
|
||||
statut: StatutUtilisateurType.ACTIF,
|
||||
password: 'secret',
|
||||
} as Users;
|
||||
|
||||
const parent = {
|
||||
user_id: 'u1',
|
||||
numero_dossier: '2026-000042',
|
||||
user: {
|
||||
id: 'u1',
|
||||
email: 'p@b.fr',
|
||||
prenom: 'Paul',
|
||||
nom: 'Parent',
|
||||
role: RoleType.PARENT,
|
||||
password: 'secret',
|
||||
} as Users,
|
||||
co_parent: coParent,
|
||||
parentChildren: [],
|
||||
} as Parents;
|
||||
|
||||
const out = mapParentForApi(parent);
|
||||
expect(out.co_parent?.prenom).toBe('Clara');
|
||||
expect(out.co_parent?.nom).toBe('Co');
|
||||
expect(out.co_parent?.password).toBeUndefined();
|
||||
expect(out.user.password).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Parents } from 'src/entities/parents.entity';
|
||||
import { sanitizeUserForApi } from '../../common/utils/sanitize-user-for-api';
|
||||
|
||||
/**
|
||||
* Sérialisation API fiche parent — ticket #131.
|
||||
* Garantit `user`, `co_parent` (si présent) et relations sans champs sensibles.
|
||||
*/
|
||||
export function mapParentForApi(parent: Parents): Parents {
|
||||
return {
|
||||
...parent,
|
||||
user: sanitizeUserForApi(parent.user)!,
|
||||
co_parent: sanitizeUserForApi(parent.co_parent),
|
||||
};
|
||||
}
|
||||
|
||||
export function mapParentsForApi(parents: Parents[]): Parents[] {
|
||||
return parents.map(mapParentForApi);
|
||||
}
|
||||
Reference in New Issue
Block a user