From c6cf51c59d0cef3ce3c2e3659494522882cba34c Mon Sep 17 00:00:00 2001
From: hawkspar <quentin.chevalier@polytechnique.edu>
Date: Mon, 3 Dec 2018 18:13:21 +0100
Subject: [PATCH] Added clean generics, fix edit

---
 ldap_config.json      |   4 +-
 src/ldap/basics.ts    |   8 ++--
 src/ldap/group.ts     |  20 +++++---
 src/ldap/user.ts      |  96 ++++++++++++-------------------------
 src/ldap/utilities.ts | 109 ++++++++++++++++++------------------------
 5 files changed, 97 insertions(+), 140 deletions(-)

diff --git a/ldap_config.json b/ldap_config.json
index dbb5dc3..03484ee 100644
--- a/ldap_config.json
+++ b/ldap_config.json
@@ -20,7 +20,7 @@
 		"nationality": "country",
 		"promotion": "brPromo",
 		"phone": "telephoneNumber",
-		"adresses": "brRoom",
+		"adress": "brRoom",
 		"id": "uidNumber",
 		"password": "userPassword",
 		"idNum": "gidNumber",
@@ -28,7 +28,7 @@
 		"login": "loginShell",
 		"readPerm": "brNewsReadAccess",
 		"writePerm": "brNewsPostAccess",
-		"mails": "mail",
+		"mail": "mail",
 		"ips": "brIP",
 		"forlifes": "brAlias",
 		"groups": "brMemberOf",
diff --git a/src/ldap/basics.ts b/src/ldap/basics.ts
index e2ab6e0..1ce3179 100644
--- a/src/ldap/basics.ts
+++ b/src/ldap/basics.ts
@@ -117,7 +117,7 @@ export class LDAP {
                 // Si la recherche renvoie une erreur, on renvoit
                 res.on('error', resErr => { throw resErr; });
                 // Si la recherche est finie on se déconnecte
-                res.on('end', res => { LDAP.unbind(); });
+                res.on('end', _ => { LDAP.unbind(); });
             }
         });
         // On renvoit le résultat
@@ -130,7 +130,7 @@ export class LDAP {
      * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode modify).
      * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
      * @arg {string} id - Identifiant unique de la feuille à modifier
-     * @arg {string} op - Operation à réaliser sur le LDAP. Trois opération sont possibles ; "add", qui rajoute des attributs et qui peut créer des doublons,
+     * @arg {"add"|"del"|"replace"} op - Operation à réaliser sur le LDAP. Trois opération sont possibles ; "add", qui rajoute des attributs et qui peut créer des doublons,
      * "del" qui en supprime, et "replace" qui remplace du contenu par un autre. 
      * @arg {Object.<string, string>} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs.
      * @arg {Object} mod[key] - Nouvelle valeur de l'attribut key. Une nouvelle valeur vide ("") est équivalent à la suppression de cet attribut.
@@ -138,14 +138,14 @@ export class LDAP {
      * @static
      * @async
      */
-    static async change(domain: 'gr'|'us', id: string, op: string, mod) : Promise<boolean> {
+    static async change(domain: 'gr'|'us', id: string, op: "add"|"del"|"replace", mod) : Promise<boolean> {
         LDAP.adminBind();
         let dn = ldapConfig.key_id+'='+id+','
         if (domain == "gr") { dn+=ldapConfig.dn_groups }
         else                { dn+=ldapConfig.dn_users }
         // Modification LDAP selon ldapConfiguration en argument (pourrait prendre une liste de Changes)
         client.modify(ldapEscape.dn("${txt}", {txt: dn}), new ldap.Change({
-            operation: ldapEscape.dn("${txt}", {txt: op}),
+            operation: op,
             modification: mod,
         // Gestion erreur 
         }), err => {
diff --git a/src/ldap/group.ts b/src/ldap/group.ts
index b32643c..91dbaf6 100644
--- a/src/ldap/group.ts
+++ b/src/ldap/group.ts
@@ -67,7 +67,7 @@ export class Group {
     */
     static async search(input: string) : Promise<string[]> {
         try {
-            return Tools.genericSearch<groupData>("gr", {
+            return Tools.genericSearch("gr", {
                 "gid": input,
                 "name": "",
                 "type": "",
@@ -354,17 +354,25 @@ export class Group {
     }
 
     /**
-     * @summary Fonction qui édite un groupe existant dans le LDAP. Très similaire à {@link User.addGroup}
-     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link Admin.addGroupMember} et {@link Admin.addGroupAdmin} en godmode pour gérer les groupes du nouvel utilisateur.
-     * @arg {groupData} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels...
-     * Sauf 'gid', qui permet de savoir quel groupe modifier et qui est donc inchangeable. On peut modifier nickname par contre.
+     * @summary Fonction qui édite un groupe existant dans le LDAP.
+     * @desc Appelle {@link genericEdit} bien sûr, mais aussi {@link addMember} et {@link addAdmin}.
+     * @arg {groupData} data - Dictionnaire des informations du groupe.
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @async
      * @static
      */
     static async edit(data: groupData) : Promise<boolean> {
         try {
-            return Tools.genericEdit<groupData>("gr",data);
+            let gid = data["gid"];
+            // Remove old members and admins
+            let profil = Group.peek(gid);
+            profil["members"].forEach(uid => { Group.remMember(uid, gid); });
+            profil["admins"].forEach(uid => { Group.remAdmin(uid, gid); });
+            // Add new members and admins
+            data["members"].forEach(uid => { Group.addMember(uid, gid); });
+            data["admins"].forEach(uid => { Group.addAdmin(uid, gid); });
+            // Edit all other fields
+            return Tools.genericEdit("gr",data);
         }
         catch(err) {
             throw "Erreur lors de la modification d'un groupe.";
diff --git a/src/ldap/user.ts b/src/ldap/user.ts
index 20c9a90..35fadd5 100644
--- a/src/ldap/user.ts
+++ b/src/ldap/user.ts
@@ -35,26 +35,26 @@ import {Group} from './group';
  * TBA @var {string[]} likes - Liste des gid dont l'utilisateur est sympathisant
  */
 export interface userData {
-    "uid"?: string,
+    "uid": string,
+    "groups": string[],
+    "groupsIsAdmin": string[],
+    "password"?: string,
     "givenName"?: string,
     "lastName"?: string,
     "nickname"?: string,
+    "promotion"?: string,
     "photo"?: string,
     "birthdate"?: string,
     //"nationality"?: string,
-    "promotion"?: string,
     "phone"?: string,
-    "adresses"?: string,
-    "mails"?: string[],
-    "groups"?: string[],
-    "password"?: string,
+    "adress"?: string,
+    "mail"?: string,
     "ips"?: string[],
     "directory"?: string,
     "login"?: string,
     "readPerm"?: string,
     "writePerm"?: string,
-    "forlifes"?: string[],
-    "admins"?: string[]
+    "forlifes"?: string[]
     //"likes"?: string[]
 }
 
@@ -68,49 +68,26 @@ export class User {
      * @summary Constructeur vide.
      */
     constructor() {}
-    
+     
     /**
-     * @summary Fonction qui renvoit certaines informations relatives à un utilisateur particulier.
-     * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
+     * @summary Fonction qui renvoit les infos de base relatives à un utilisateur particulier.
+     * @desc Cette fonction utilise {@link Tools.genericPeek} avec l'interface {@link userData}.
      * @arg {string} uid - Identifiant de l'utilisateur
-     * @return {Promise(T)} Informations recueillies ; renvoie une partie du profil de l'utilisateur selon le format choisi.
-     * Voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes.
+     * @return {Promise(userData)} Informations recueillies.
      * @static
      * @async
-     * @private
      */
-    private static async genericPeek<T>(uid: string) : Promise<T> {
-        try {
-            let fields = [];
-            fields.push(ldapConfig.user.values());
-            let LDAPUserData = await LDAP.search("us", fields, uid);
-            let cleanUserData : T;
-            // Rename output
-            for (let uncleanKey in LDAPUserData) {
-                for (let cleanKey in cleanUserData) {
-                    if (uncleanKey==ldapConfig.group[cleanKey]) { cleanUserData[cleanKey] = LDAPUserData[uncleanKey]; }
-                }
-            }
-            return cleanUserData;
+    static async peek(uid: string) : Promise<userData> {
+        try { 
+            return Tools.genericPeek<userData>("us", uid);
         }
         catch(err) {
-            throw "Erreur lors d'une recherche d'informations sur un individu.";
+            throw "Error while peeking a user.";
         }
     }
-     
-    /**
-     * @summary Fonction qui renvoit les infos de base relatives à un utilisateur particulier.
-     * @desc Cette fonction utilise {@link User.genericPeek} avec l'interface {@link userData}.
-     * @arg {string} uid - Identifiant de l'utilisateur
-     * @return {Promise(userData)} Informations recueillies.
-     * @static
-     * @async
-     */
-    static async peek(uid: string) : Promise<userData> { return User.genericPeek<userData>(uid); }
     
     /**
-     * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Autre étape vers vrai TOL (Trombino On Line). Doit être préféré à repliquer TOL
-     * car moins gourmande envers le LDAP (utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos).
+     * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Utiliser {@link peek} au cas par cas après pour obtenir les vraies infos.
      * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs
      * mais pas approximatifs et ne gère pas l'auto-complete. MEF Timeout pour des recherches trop vagues. Va crasher si un champ n'est pas dans ldapConfig.
      * Utiliser trouverGroupesParTypes pour chaque champ relié à groups.
@@ -123,25 +100,7 @@ export class User {
      */
     static async search(data: userData) : Promise<string[]> {
         try {
-            let filter="";
-            // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans ldapConfig encore
-            for (var key in data) {
-                if ((data[key]!= undefined) && (data[key] != '')) {                    // Si il y a qque chose à chercher pour ce filtre
-                    if (!Array.isArray(data[key])) { data[key]=[data[key]]; }          // Gestion d'une liste de valeurs à rechercher
-                    // Iteration pour chaque valeur fournie par l'utilisateur
-                    data[key].forEach(val => {
-                        // Traduction en language LDAP
-                        let attribute = ldapConfig.user[key];
-                        // Creation incrémentale du filtre
-                        filter="(&"+filter+ "(|("+attribute+"="+ val+")"+      // On cherche la valeur exacte
-                                            "(|("+attribute+"=*"+val+")"+      // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs)
-                                            "(|("+attribute+"=*"+val+"*)"+     // La valeur du milieu avec des trucs avant et après
-                                            "("+  attribute+"="+ val+"*)))))"; // La valeur du début avec des trucs après
-                    });
-                }
-            }
-            // Appel avec filtre de l'espace 
-            return LDAP.search("us", [ldapConfig.key_id], null, filter);
+            return Tools.genericSearch("us", data);
         }
         catch(err) {
             throw "Erreur lors de la recherche approximative d'un utilisateur.";
@@ -298,19 +257,24 @@ export class User {
 
     /**
      * @summary Fonction qui édite un utilisateur existant dans le LDAP. Très similaire à {@link creerUtilisateur}
-     * @desc Appelle simplement {@link creerUtilisateur} et {@link supprimerUtilisateur} en godmode, plus {@link renseignerSurUtilisateur} pour les champs non fournis.
-     * Ce choix a pour conséquence que l'ordre du dictionnaire de correspondance dans ldap_ldapConfig est important.
-     * Une version "nerfée" de cette fonction est envisageable ; elle donne bcp de pouvoir à l'utilisateur.
-     * @arg {fullUserData} data - Dictionnaire des informations utilisateurs au même format que pour {@link creerUtilisateur} avec tous les champs optionnels sauf 'uid',
-     * qui permet de savoir qui modifier. Attention toutes les clés de cette entrée seront modifiées dans le LDAP ; les nouveaux résultats écrasant les précédents,
-     * sauf 'readPerm','writePerm', 'forlifes','ips','groups' et 'groupsIsAdmin' qui sont censurés pour cette fonction).
+     * @desc Appelle simplement {@link genericEdit}.
+     * @arg {userData} data - Dictionnaire des informations utilisateurs
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @async
      * @static
      */
     static async edit(data : userData) : Promise<boolean> {
         try {
-            return Tools.genericEdit<userData>("us",data);
+            let uid = data["uid"];
+            // Leave old groups
+            let profil = User.peek(uid);
+            profil["groups"].forEach(gid => { Group.remMember(uid, gid); });
+            profil["groupsIsAdmin"].forEach(gid => { Group.remAdmin(uid, gid); });
+            // Join new groups
+            data["groups"].forEach(gid => { Group.addMember(uid, gid); });
+            data["groupsIsAdmin"].forEach(gid => { Group.addAdmin(uid, gid); });
+            // Edit all other fields
+            return Tools.genericEdit("us",data);
         }
         catch(err) {
             throw "Erreur lors de la modification d'un utilisateur.";
diff --git a/src/ldap/utilities.ts b/src/ldap/utilities.ts
index 55fb907..dc47a12 100644
--- a/src/ldap/utilities.ts
+++ b/src/ldap/utilities.ts
@@ -6,10 +6,8 @@
 
 import {ldapConfig} from './config';
 import {LDAP} from './basics';
-import { groupData } from './group';
 import { userData } from './user';
-import { Group } from './group';
-import { User } from './user';
+import { groupData } from './group';
 
 //------------------------------------------------------------------------------------------------------------------------
 // Fonctions intermédiaires TBT
@@ -26,23 +24,26 @@ export class Tools {
     /**
      * @summary Fonction qui renvoit toutes les infos relatives à un groupe ou un utilisateur particulier.
      * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
-     * @arg {string} domain - Domaine de la recherche
-     * @arg {string} id - Identifiant de la feuille cherchée
-     * @return {Promise(T)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ;
-     * voir `ldap_config.json`(..\..\ldap_config.json) pour les clés exactes.
+     * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData})
+     * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe)
+     * @arg {string} id - Identifiant de la feuille cherchée (uid ou gid)
+     * @return {Promise(T)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe tel que défini par le paramètre T.
      * @static
      * @async
      */
-    static async genericPeek<T>(domain: 'us' | 'gr', id: string) : Promise<T> {
-        let fields = [];
-        if (domain=='gr')   { fields.push(ldapConfig.group.values()); }
-        else                { fields.push(ldapConfig.user.values()); }
-        let LDAPData = await LDAP.search(domain, fields, id);
+    static async genericPeek<T>(domain: 'us'|'gr', id: string) : Promise<T> {
+        if (domain='gr') {
+            var dirtyKeys = ldapConfig.group;
+        }
+        else {
+            var dirtyKeys = ldapConfig.user;
+        }
         let cleanData : T;
+        let dirtyData = await LDAP.search(domain, dirtyKeys.values(), id);
         // Rename output
-        for (let uncleanKey in LDAPData) {
+        for (let uncleanKey in dirtyData) {
             for (let cleanKey in cleanData) {
-                if (uncleanKey==ldapConfig.group[cleanKey]) { cleanData[cleanKey] = LDAPData[uncleanKey]; }
+                if (uncleanKey=dirtyKeys[cleanKey]) { cleanData[cleanKey] = dirtyData[uncleanKey]; }
             }
         }
         return cleanData;
@@ -50,29 +51,30 @@ export class Tools {
 
     
     /**
-     * @summary Fonction qui retrouve les uid des paxs validant les critères de recherche. Autre étape vers vrai TOL (Trombino On Line). Doit être préféré à repliquer TOL
-     * car moins gourmande envers le LDAP (utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos).
+     * @summary Fonction qui retrouve les id des paxs ou groupes validant les critères de recherche. Etape vers vrai TOL (Trombino On Line).
+     * Utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos.
      * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs
      * mais pas approximatifs et ne gère pas l'auto-complete. MEF Timeout pour des recherches trop vagues. Va crasher si un champ n'est pas dans ldapConfig.
-     * Utiliser trouverGroupesParTypes pour chaque champ relié à groups.
-     * @arg {T} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse
-     * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exempl pour chercher un membre
+     * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData})
+     * @arg {string} domain - Domaine de la recherche (utilisateur ou groupe)
+     * @arg {userData | groupData} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse
+     * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exemple pour chercher un membre
      * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined.
-     * @return {Promise(string[])} gids des profils qui "match" les critères proposés.
+     * @return {Promise(string[])} ids des profils qui "match" les critères proposés.
      * @static
      * @async
      */
-    static async genericSearch<T>(domain : "us"|"gr", data : T) : Promise<string[]> {
+    static async genericSearch(domain : "us"|"gr", data : userData|groupData) : Promise<string[]> {
         let filter="";
         // Iteration pour chaque champ, alourdissement du filtre selon des trucs prédéfinis dans ldapConfig encore
         for (var key in data) {
             if ((data[key]!= undefined) && (data[key] != '')) {                    // Si il y a qque chose à chercher pour ce filtre
-                if (!Array.isArray(data[key])) { data[key]=[data[key]]; }          // Gestion d'une liste de valeurs à rechercher
+                if (!Array.isArray(data[key])) { data[key]=[data[key]]; }          // Génération systématique d'une liste de valeurs à rechercher
                 // Iteration pour chaque valeur fournie par l'utilisateur
                 data[key].forEach(val => {
                     // Traduction en language LDAP
                     let attribute = "";
-                    if (domain=="us")   { attribute = ldapConfig.user[key]; }
+                    if (domain="us")    { attribute = ldapConfig.user[key]; }
                     else                { attribute = ldapConfig.group[key]; }
                     // Creation incrémentale du filtre
                     filter="(&"+filter+ "(|("+attribute+"="+ val+")"+      // On cherche la valeur exacte
@@ -85,50 +87,33 @@ export class Tools {
         // Appel avec filtre de l'espace 
         return LDAP.search(domain, [ldapConfig.key_id], null, filter);
     }
-
     
     /**
-     * @summary Fonction qui édite un groupe existant dans le LDAP. Très similaire à {@link User.addGroup}
-     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link Admin.addGroupMember} et {@link Admin.addGroupAdmin} en godmode pour gérer les groupes du nouvel utilisateur.
-     * @arg {T} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels...
+     * @summary Fonction qui édite un groupe existant dans le LDAP.
+     * @desc Appelle {@link LDAP.change}.
+     * @arg {userData | groupData} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels...
      * Sauf 'gid', qui permet de savoir quel groupe modifier et qui est donc inchangeable. On peut modifier nickname par contre.
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @async
      * @static
      */
-    static async genericEdit<T>(domain: "us" | "gr", data: T) : Promise<boolean> {
-        let id = "";
-        if (domain == "us")     { id=data['uid']; }
-        else                    { id=data['gid']; }
-        // Récupération des anciennes données
-        let profil = await Tools.genericPeek<T>(domain,id);
-        if (domain == "us") {
-            // Régénération du champ manquant dans profil
-            let lg = await Tools.getGroups(id);
-            profil['groupsIsAdmin']=[];
-            lg.forEach(gid => {
-                Tools.isGroupAdmin(id, gid).then(res => {
-                    if (res) { profil['groupsIsAdmin'].push(gid); }
-                });
-            });
+    static async genericEdit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> {
+        if (domain = "us") {
+            var id=data['uid'];
+            var dirtyKeys=ldapConfig.user;
+        }
+        else {
+            var id=data['gid'];
+            var dirtyKeys=ldapConfig.group;
         }
-        // Surcharge des champs à modifier selon data
-        Object.keys(data).forEach(function(key: string) {
-            // Some fields the user cannot change
-            if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) { profil[key]=data[key]; }
-            // Specialised management of group membership and admin status
-            if (key=="groups") { data[key].array.forEach(gid => { Group.addMember(id, gid); }); }
-            if (key=="groupsIsAdmin") { data[key].array.forEach(gid => { Group.addAdmin(id, gid); }); }
-            if (key=="members") { data[key].array.forEach(uid => { Group.addMember(uid, id); }); }
-            if (key=="admins") { data[key].array.forEach(uid => { Group.addMember(uid, id); }); }
-        });
         // Renommage LDAP-friendly
-        let dirtyProfil = {};
-        Object.keys(profil).forEach(function(key: string) {
-            if (domain=="gr")   { dirtyProfil[ldapConfig.group.key]=profil[key]; }
-            else                { dirtyProfil[ldapConfig.user.key]=profil[key]; }
+        let dirtyData = {};
+        Object.keys(data).forEach(function(key: string) {
+            if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) {
+                dirtyData[dirtyKeys.key]=data[key];
+            }
         });
-        return LDAP.change(domain,id,"replace",dirtyProfil);
+        return LDAP.change(domain,id,"replace",dirtyData);
     }
 
     /**
@@ -158,7 +143,7 @@ export class Tools {
             return LDAP.search(domain, [ldapConfig.key_id], null, "("+attribute+"="+value+")").then(function (matches: string[]) {
                 if (!matches) { throw ""; }
                 // On renvoit la valeur si elle est bien unique
-                else if (matches.length==0) { return value; }
+                else if (matches.length=0) { return value; }
                 // Sinon, on tente de nouveau notre chance avec la valeur suivante
                 else { return Tools.ensureUnique(changeValue(value, n+1), attribute, domain, changeValue, n+1); }
             });
@@ -182,8 +167,8 @@ export class Tools {
         try {
             // normalize et lowerCase standardisent le format
             return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "us", (id: string, n: number) => {
-                if (n==1) { id+='.'+promotion; }                // Si prénom.nom existe déjà, on rajoute la promo
-                else if (n==2) { id+='.'+(n-1).toString(); }    // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1
+                if (n=1) { id+='.'+promotion; }                // Si prénom.nom existe déjà, on rajoute la promo
+                else if (n=2) { id+='.'+(n-1).toString(); }    // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1
                 else if (n>2) { id+=n; }                        // Ensuite on continue .123, .1234, etc...
                 return id;
             });
@@ -205,8 +190,8 @@ export class Tools {
         try {
             // normalize et lowerCase standardisent le format
             return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "gr", (id: string, n: number) => {
-                if (n==1) { id+='.'+n.toString(); }   // Si nom existe déjà, on essaie nom.1
-                else if (n>1) { id+=n.toString(); }   // Ensuite on continue .12, .123, etc...
+                if (n=1)        { id+='.'+n.toString(); }   // Si nom existe déjà, on essaie nom.1
+                else if (n>1)   { id+=n.toString(); }       // Ensuite on continue .12, .123, etc...
                 return id;
             });
         }
-- 
GitLab