fix: GET /parents/pending-families 500 + #113 doublons inscription
- parents.service: normaliser parentIds (array ou string PG) pour éviter 500 - auth.service: doublons à l'inscription (#113) - parent/co-parent même email, NIR et numéro agrément AM - docs: mise à jour statuts tickets Made-with: Cursor
This commit is contained in:
parent
d832559027
commit
7e9306de01
@ -43,6 +43,8 @@ export class AuthService {
|
||||
private readonly usersRepo: Repository<Users>,
|
||||
@InjectRepository(Children)
|
||||
private readonly childrenRepo: Repository<Children>,
|
||||
@InjectRepository(AssistanteMaternelle)
|
||||
private readonly assistantesMaternellesRepo: Repository<AssistanteMaternelle>,
|
||||
) { }
|
||||
|
||||
/**
|
||||
@ -189,6 +191,11 @@ export class AuthService {
|
||||
}
|
||||
|
||||
if (dto.co_parent_email) {
|
||||
if (dto.email.trim().toLowerCase() === dto.co_parent_email.trim().toLowerCase()) {
|
||||
throw new BadRequestException(
|
||||
'L\'email du parent et du co-parent doivent être différents.',
|
||||
);
|
||||
}
|
||||
const coParentExiste = await this.usersService.findByEmailOrNull(dto.co_parent_email);
|
||||
if (coParentExiste) {
|
||||
throw new ConflictException('L\'email du co-parent est déjà utilisé');
|
||||
@ -360,6 +367,27 @@ export class AuthService {
|
||||
throw new ConflictException('Un compte avec cet email existe déjà');
|
||||
}
|
||||
|
||||
const nirDejaUtilise = await this.assistantesMaternellesRepo.findOne({
|
||||
where: { nir: nirNormalized },
|
||||
});
|
||||
if (nirDejaUtilise) {
|
||||
throw new ConflictException(
|
||||
'Un compte assistante maternelle avec ce numéro NIR existe déjà.',
|
||||
);
|
||||
}
|
||||
|
||||
const numeroAgrement = (dto.numero_agrement || '').trim();
|
||||
if (numeroAgrement) {
|
||||
const agrementDejaUtilise = await this.assistantesMaternellesRepo.findOne({
|
||||
where: { approval_number: numeroAgrement },
|
||||
});
|
||||
if (agrementDejaUtilise) {
|
||||
throw new ConflictException(
|
||||
'Un compte assistante maternelle avec ce numéro d\'agrément existe déjà.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const joursExpirationToken = await this.appConfigService.get<number>(
|
||||
'password_reset_token_expiry_days',
|
||||
7,
|
||||
|
||||
@ -79,47 +79,63 @@ export class ParentsService {
|
||||
* Uniquement les parents dont l'utilisateur a statut = en_attente.
|
||||
*/
|
||||
async getPendingFamilies(): Promise<PendingFamilyDto[]> {
|
||||
const raw = await this.parentsRepository.query(`
|
||||
WITH RECURSIVE
|
||||
links AS (
|
||||
SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT ep1.id_parent AS p1, ep2.id_parent AS p2
|
||||
FROM enfants_parents ep1
|
||||
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
||||
UNION ALL
|
||||
SELECT ep2.id_parent AS p1, ep1.id_parent AS p2
|
||||
FROM enfants_parents ep1
|
||||
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
||||
),
|
||||
rec AS (
|
||||
SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents
|
||||
UNION
|
||||
SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1
|
||||
),
|
||||
family_rep AS (
|
||||
SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id
|
||||
)
|
||||
SELECT
|
||||
'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
|
||||
array_agg(DISTINCT p.id_utilisateur ORDER BY p.id_utilisateur) AS "parentIds",
|
||||
(array_agg(p.numero_dossier))[1] AS numero_dossier
|
||||
FROM family_rep fr
|
||||
JOIN parents p ON p.id_utilisateur = fr.id
|
||||
JOIN utilisateurs u ON u.id = p.id_utilisateur
|
||||
WHERE u.role = 'parent' AND u.statut = 'en_attente'
|
||||
GROUP BY fr.rep
|
||||
ORDER BY libelle
|
||||
`);
|
||||
return raw.map((r: { libelle: string; parentIds: unknown; numero_dossier: string | null }) => ({
|
||||
libelle: r.libelle,
|
||||
parentIds: Array.isArray(r.parentIds) ? r.parentIds.map(String) : [],
|
||||
let raw: { libelle: string; parentIds: unknown; numero_dossier: string | null }[];
|
||||
try {
|
||||
raw = await this.parentsRepository.query(`
|
||||
WITH RECURSIVE
|
||||
links AS (
|
||||
SELECT p.id_utilisateur AS p1, p.id_co_parent AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT p.id_co_parent AS p1, p.id_utilisateur AS p2 FROM parents p WHERE p.id_co_parent IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT ep1.id_parent AS p1, ep2.id_parent AS p2
|
||||
FROM enfants_parents ep1
|
||||
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
||||
UNION ALL
|
||||
SELECT ep2.id_parent AS p1, ep1.id_parent AS p2
|
||||
FROM enfants_parents ep1
|
||||
JOIN enfants_parents ep2 ON ep2.id_enfant = ep1.id_enfant AND ep1.id_parent < ep2.id_parent
|
||||
),
|
||||
rec AS (
|
||||
SELECT id_utilisateur AS id, id_utilisateur AS rep FROM parents
|
||||
UNION
|
||||
SELECT l.p2 AS id, LEAST(rec_alias.rep, l.p2) AS rep FROM links l JOIN rec rec_alias ON rec_alias.id = l.p1
|
||||
),
|
||||
family_rep AS (
|
||||
SELECT id, (MIN(rep::text))::uuid AS rep FROM rec GROUP BY id
|
||||
)
|
||||
SELECT
|
||||
'Famille ' || string_agg(u.nom, ' - ' ORDER BY u.nom, u.prenom) AS libelle,
|
||||
array_agg(DISTINCT p.id_utilisateur ORDER BY p.id_utilisateur) AS "parentIds",
|
||||
(array_agg(p.numero_dossier))[1] AS numero_dossier
|
||||
FROM family_rep fr
|
||||
JOIN parents p ON p.id_utilisateur = fr.id
|
||||
JOIN utilisateurs u ON u.id = p.id_utilisateur
|
||||
WHERE u.role = 'parent' AND u.statut = 'en_attente'
|
||||
GROUP BY fr.rep
|
||||
ORDER BY libelle
|
||||
`);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.map((r) => ({
|
||||
libelle: r.libelle ?? '',
|
||||
parentIds: this.normalizeParentIds(r.parentIds),
|
||||
numero_dossier: r.numero_dossier ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
/** Convertit parentIds (array ou chaîne PG) en string[] pour éviter 500 si le driver renvoie une chaîne. */
|
||||
private normalizeParentIds(parentIds: unknown): string[] {
|
||||
if (Array.isArray(parentIds)) return parentIds.map(String);
|
||||
if (typeof parentIds === 'string') {
|
||||
const s = parentIds.replace(/^\{|\}$/g, '').trim();
|
||||
return s ? s.split(',').map((x) => x.trim()) : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les user_id de tous les parents de la même famille (co_parent ou enfants partagés).
|
||||
* @throws NotFoundException si parentId n'est pas un parent
|
||||
|
||||
@ -33,13 +33,13 @@
|
||||
| 20 | [Backend] API Inscription Parent (étape 3 - Enfants) | ✅ Terminé |
|
||||
| 21 | [Backend] API Inscription Parent (étape 4-6 - Finalisation) | ✅ Terminé |
|
||||
| 24 | [Backend] API Création mot de passe | Ouvert |
|
||||
| 25 | [Backend] API Liste comptes en attente | Ouvert |
|
||||
| 26 | [Backend] API Validation/Refus comptes | Ouvert |
|
||||
| 27 | [Backend] Service Email - Installation Nodemailer | Ouvert |
|
||||
| 25 | [Backend] API Liste comptes en attente | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 26 | [Backend] API Validation/Refus comptes | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 27 | [Backend] Service Email - Installation Nodemailer | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 28 | [Backend] Templates Email - Validation | Ouvert |
|
||||
| 29 | [Backend] Templates Email - Refus | Ouvert |
|
||||
| 30 | [Backend] Connexion - Vérification statut | Ouvert |
|
||||
| 31 | [Backend] Changement MDP obligatoire première connexion | Ouvert |
|
||||
| 29 | [Backend] Templates Email - Refus | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 30 | [Backend] Connexion - Vérification statut | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 31 | [Backend] Changement MDP obligatoire première connexion | ✅ Terminé |
|
||||
| 32 | [Backend] Service Documents Légaux | Ouvert |
|
||||
| 33 | [Backend] API Documents Légaux | Ouvert |
|
||||
| 34 | [Backend] Traçabilité acceptations documents | Ouvert |
|
||||
@ -53,8 +53,8 @@
|
||||
| 42 | [Frontend] Inscription AM - Finalisation | ✅ Terminé |
|
||||
| 43 | [Frontend] Écran Création Mot de Passe | Ouvert |
|
||||
| 44 | [Frontend] Dashboard Gestionnaire - Structure | ✅ Terminé |
|
||||
| 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | Ouvert |
|
||||
| 46 | [Frontend] Dashboard Gestionnaire - Liste AM | Ouvert |
|
||||
| 45 | [Frontend] Dashboard Gestionnaire - Liste Parents | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 46 | [Frontend] Dashboard Gestionnaire - Liste AM | ✅ Fermé (obsolète, couvert #103-#111) |
|
||||
| 47 | [Frontend] Écran Changement MDP Obligatoire | Ouvert |
|
||||
| 48 | [Frontend] Gestion Erreurs & Messages | Ouvert |
|
||||
| 49 | [Frontend] Écran Gestion Documents Légaux (Admin) | Ouvert |
|
||||
@ -641,17 +641,18 @@ Modifier l'endpoint de connexion pour bloquer les comptes en attente ou suspendu
|
||||
|
||||
---
|
||||
|
||||
### Ticket #31 : [Backend] Changement MDP obligatoire première connexion
|
||||
### Ticket #31 : [Backend] Changement MDP obligatoire première connexion ✅
|
||||
**Estimation** : 2h
|
||||
**Labels** : `backend`, `p2`, `auth`, `security`
|
||||
**Statut** : ✅ TERMINÉ
|
||||
|
||||
**Description** :
|
||||
Implémenter le changement de mot de passe obligatoire pour les gestionnaires/admins à la première connexion.
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Endpoint `POST /api/v1/auth/change-password-required`
|
||||
- [ ] Vérification flag `changement_mdp_obligatoire`
|
||||
- [ ] Mise à jour flag après changement
|
||||
- [x] Endpoint `POST /api/v1/auth/change-password-required`
|
||||
- [x] Vérification flag `changement_mdp_obligatoire`
|
||||
- [x] Mise à jour flag après changement
|
||||
- [ ] Tests unitaires
|
||||
|
||||
---
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user