diff --git a/README.md b/README.md
index 0252299cfd73570d02d228f8dc7d0bd650921dd5..16a92272a1d0d7d9eb93d215adb02863633b7bb4 100644
--- a/README.md
+++ b/README.md
@@ -196,7 +196,6 @@ On peut définir ces variables d'environnement, **dans l'ordre décroissant de p
     ...
     ```
 
-
 ## Panneau d'administration
 
 Il est accessible par navigateur au path `/adminview/admin` ; n'importe quel path devrait rediriger dessus. 
diff --git a/configfile_doc.json b/configfile_doc.json
index 3c4db54b4e44b38898249152e450588b681da74e..e2a217abfdb2c74f3bf9714af0d2a26008ccbd10 100644
--- a/configfile_doc.json
+++ b/configfile_doc.json
@@ -27,6 +27,6 @@
       "extensions": ["ts", "tsx"],
       "babelrc": false,
       "presets": [["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/typescript"],
-      "plugins": ["@babel/proposal-class-properties", "@babel/proposal-object-rest-spread"]
+      "plugins": ["@babel/plugin-proposal-class-properties", "@babel/proposal-object-rest-spread"]
     }
 }
\ No newline at end of file
diff --git a/ldap_config.json b/ldap_config.json
index 57370d5e8279fa05723aa4f9c3e9290246b161d7..03484ee0cd6facfe2060d30a43f8ced9107d6672 100644
--- a/ldap_config.json
+++ b/ldap_config.json
@@ -9,54 +9,46 @@
 	
 	"comment_3": "Placeholders et indications de contenu de certains champs du LDAP généré par frankiz pour les utilisateurs",
 	"user": {
-		"single": {
-			"photo": "jpegPhoto",
-			"givenName": "givenName",
-			"lastName": "sn",
-			"fullName": "cn",
-			"cleanFullName": "gecos",
-			"nickname": "displayName",
-			"birthdate": "brBirthdate",
-			"nationality": "country",
-			"promotion": "brPromo",
-			"phone": "telephoneNumber",
-			"adress": "brRoom",
-			"id": "uidNumber",
-			"sport": "brMemberOf",
-			"password": "userPassword",
-			"idNum": "gidNumber",
-			"directory": "homeDirectory",
-			"login": "loginShell",
-			"readPerm": "brNewsReadAccess",
-			"writePerm": "brNewsPostAccess"
-		},
-		"multiple": {
-			"mail": "mail",
-			"ip": "brIP",
-			"forlifes": "brAlias",
-			"groups": "brMemberOf",
-			"school": "brMemberOf",
-			"course": "brMemberOf",
-			"class": "objectClass"
-		}
+		"uid": "uid",
+		"photo": "jpegPhoto",
+		"givenName": "givenName",
+		"lastName": "sn",
+		"fullName": "cn",
+		"cleanFullName": "gecos",
+		"nickname": "displayName",
+		"birthdate": "brBirthdate",
+		"nationality": "country",
+		"promotion": "brPromo",
+		"phone": "telephoneNumber",
+		"adress": "brRoom",
+		"id": "uidNumber",
+		"password": "userPassword",
+		"idNum": "gidNumber",
+		"directory": "homeDirectory",
+		"login": "loginShell",
+		"readPerm": "brNewsReadAccess",
+		"writePerm": "brNewsPostAccess",
+		"mail": "mail",
+		"ips": "brIP",
+		"forlifes": "brAlias",
+		"groups": "brMemberOf",
+		"classes": "objectClass"
 	},
 	"comment_4": "Placeholders et indications de contenu de certains champs du LDAP généré par frankiz pour les groupes",
 	"group": {
-		"single": {
-			"name": "cn",
-			"nickname": "brAlias",
-			"type": "brNS",
-			"idNumber": "uidNumber",
-			"idNumber2": "gidNumber",
-			"login": "loginShell",
-			"password": "userPassword",
-			"directory": "homeDirectory",
-			"cleanFullName": "gecos"
-		},
-		"multiple": {
-			"member": "restrictedMemberUid",
-			"admin": "memberUid",
-			"class": "objectClass"
-		}
-	}
+		"gid": "uid",
+		"name": "brAlias",
+		"type": "brNS",
+		"members": "restrictedMemberUid",
+		"admins": "memberUid",
+		"adress":"cn",
+		"idNumber": "uidNumber",
+		"idNumber2": "gidNumber",
+		"login": "loginShell",
+		"password": "userPassword",
+		"directory": "homeDirectory",
+		"cleanFullName": "gecos",
+		"classes": "objectClass"
+	},
+	"sessionSecret":"ozyNMHdT,WFTu|t"
 }
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index cc9e93eb3715ad11a37bfa3f82476fecb1cc041c..1afc35330e24ddb34796d256b64734cb391dee76 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4748,13 +4748,21 @@
       }
     },
     "cross-fetch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.0.0.tgz",
-      "integrity": "sha512-gnx0GnDyW73iDq6DpqceL8i4GGn55PPKDzNwZkopJ3mKPcfJ0BUIXBsnYfJBVw+jFDB+hzIp2ELNRdqoxN6M3w==",
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz",
+      "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=",
       "dev": true,
       "requires": {
-        "node-fetch": "2.0.0",
-        "whatwg-fetch": "2.0.3"
+        "node-fetch": "2.1.2",
+        "whatwg-fetch": "2.0.4"
+      },
+      "dependencies": {
+        "whatwg-fetch": {
+          "version": "2.0.4",
+          "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
+          "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==",
+          "dev": true
+        }
       }
     },
     "cross-spawn": {
@@ -6859,12 +6867,12 @@
       }
     },
     "graphql-request": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.6.0.tgz",
-      "integrity": "sha512-qqAPLZuaGlwZDsMQ2FfgEyZMcXFMsPPDl6bQQlmwP/xCnk1TqxkE1S644LsHTXAHYPvmRWsIimfdcnys5+o+fQ==",
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz",
+      "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==",
       "dev": true,
       "requires": {
-        "cross-fetch": "2.0.0"
+        "cross-fetch": "2.2.2"
       }
     },
     "graphql-subscriptions": {
@@ -8799,9 +8807,9 @@
       }
     },
     "node-fetch": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.0.0.tgz",
-      "integrity": "sha1-mCu6Q+zU8pIqKcwYamu7C7c/y6Y=",
+      "version": "2.1.2",
+      "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
+      "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=",
       "dev": true
     },
     "node-libs-browser": {
@@ -9423,7 +9431,7 @@
         },
         "semver": {
           "version": "4.3.2",
-          "resolved": "http://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz",
           "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c="
         }
       }
diff --git a/src/graphql/typeDefs/objects.graphql b/src/graphql/typeDefs/objects.graphql
index 47ba8ea4b15433890952cfcfeb72fcaa39383d89..eca057718b39a57b80ded07094d43ecc3a694da3 100644
--- a/src/graphql/typeDefs/objects.graphql
+++ b/src/graphql/typeDefs/objects.graphql
@@ -31,11 +31,12 @@ simples, dont les membres sont des utilisateurs, et les métagroupes, dont les m
 des groupes simples (tel que Federez, dont les membres incluent le BR et DaTA). 
 """
 interface Group {
-    uid: ID
+    gid: ID!
     name: String
     # Site Web.
     website: String
     description: String
+    type: String
     
     # Jour et heure de création du groupe.
     createdAt: String!
@@ -49,9 +50,7 @@ interface Group {
     # Les questions addressees à ce groupe
     questions: [Question]
     # Les reponses donnees par ce groupe
-    answers: [Answer]
-
-
+    answers: [Answer] 
 }
 
 # Le groupe de base, dont les membres sont des utilisateurs : binets, Kès...
diff --git a/src/graphql/typeDefs/objects_ldap.graphql b/src/graphql/typeDefs/objects_ldap.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..3efc076abd01ff4b13d99061bbafc977fa3d9e64
--- /dev/null
+++ b/src/graphql/typeDefs/objects_ldap.graphql
@@ -0,0 +1,243 @@
+# hawkspar->all ; doc ?
+
+# Utilisateurs
+type User {
+    # Prénom de l'utilisateur
+    givenName: String!
+    # Nom de famille
+    lastName: String!
+    # Surnom
+    nickname: String
+    nationality: String
+    uid: ID!
+    birthdate: String!
+    mail: String
+    phone: String
+    # Groupes dont l'utilisateur est membre.
+    groups: [SimpleGroup]
+    # Groupes que l'utilisateur aime.
+    likes: [Group]
+    # A terme rajouter aussi admin
+    # Adresse(s) de l'utilisateur.
+    addresses: [String]
+    # Promotion
+    promotion: String
+    photo: String
+}
+
+# Groupes associatifs
+
+"""
+L'interface Group représente les deux types de groupes implémentés dans Sigma : les groupes
+simples, dont les membres sont des utilisateurs, et les métagroupes, dont les membres sont
+des groupes simples (tel que Federez, dont les membres incluent le BR et DaTA). 
+"""
+interface Group {
+    uid: ID
+    name: String
+    # Site Web.
+    website: String
+    description: String
+    
+    # Jour et heure de création du groupe.
+    createdAt: String!
+    # Dernière mise à jour du groupe.
+    updatedAt: String!
+
+    # member requests
+
+    # Les posts prives dans ce groupe
+    privatePosts: [PrivatePost]
+    # Les questions addressees à ce groupe
+    questions: [Question]
+    # Les reponses donnees par ce groupe
+    answers: [Answer]
+
+
+}
+
+# Le groupe de base, dont les membres sont des utilisateurs : binets, Kès...
+type SimpleGroup implements Group {
+    uid: ID
+    name: String
+    website: String
+    createdAt: String!
+    updatedAt: String!
+
+    # Admin, membres, sympathisants du groupe
+    admins: [User]
+    members: [User]
+    likers: [User]
+
+    description: String
+    # École d'origine du groupe
+    school: String
+    # Groupe parent
+    parent: Group
+
+    privatePosts: [PrivatePost]
+    questions: [Question]
+    answers: [Answer]
+}
+
+# Un groupe dont les membre sont d'autres groupes
+type MetaGroup implements Group {
+    uid: ID
+    name: String
+    website: String
+    createdAt: String!
+    updatedAt: String!
+    description: String
+
+    # Les groupes constitutifs du méta-groupe.
+    members: [Group]!
+
+    privatePosts: [PrivatePost]
+    questions: [Question]
+    answers: [Answer]
+}
+
+
+# Tout type de message adressé à un ou plusieurs groupes.
+
+# Auteur possible d'un Message
+# union AuthorUnion = Group | [Group] | User
+# union RecipientUnion = Group | [Group]
+
+# Les unions sont assez faibles dans GraphQL, 
+# elles n'acceptent pas les listes ni les interfaces
+
+# L'interface Message représente toute information que veut communiquer un groupe ou un user.
+# Par choix de paradigme, tout Message est adressé à un (ou des) groupe(s).
+# Les types implémentés sont divisés en deux :
+# - les Message émanant d'un groupe : Announcement et Event, ainsi que Answer
+# - les Message émanant d'un user : PrivatePost, ainsi que Question
+
+interface Message {
+    id: ID!
+    # Titre du message
+    title: String!
+    content: String
+    createdAt: String!
+    updatedAt: String!
+}
+
+# Annonce publique effectuée par un ou plusieurs groupes.
+type Announcement implements Message {
+    id: ID!
+    title: String!
+    createdAt: String!
+    updatedAt: String!
+    content: String!
+    importance: Int
+    views: Int
+    forEvent: Event
+
+    authors: [Group]
+    recipients: [Group]
+}
+
+# Événements organisés par un ou plusieurs groupes.
+type Event implements Message {
+    id: ID!
+    # Intitulé de l'événement
+    title: String!
+    # Lieu de l'événement
+    location: String
+    createdAt: String!
+    updatedAt: String!
+    startTime: String!
+    endTime: String!
+    # Organisateurs
+    # Personnes qui participent à l'événement.
+    participatingGroups: [Group]
+    participatingUsers: [User]
+    content: String
+    asAnnouncement: Announcement
+
+    authors: [Group]
+    recipients: [Group]
+}
+
+# Post interne d'un membre sur la page interne de son groupe
+type PrivatePost implements Message {
+    id: ID!
+    createdAt: String!
+    updatedAt: String!
+    title: String!
+    content: String!
+
+    authors: User
+    recipients: Group
+}
+
+# Question posée par un user à un groupe
+type Question implements Message {
+    id: ID!
+    createdAt: String!
+    updatedAt: String!
+    title: String!
+    content: String!
+
+    authors: User
+    recipients: Group
+
+    # Une annonce éventuellement concernée par cette question. 
+    # Null si la question ne concerne pas une annonce particulière
+
+    forAnnouncement: Announcement
+
+    # Référence la réponse donnée par le groupe à cette Question. Si pas encore répondu, null.
+    forAnswer: Answer
+}
+
+# Réponse à une Question 
+type Answer implements Message {
+    id: ID!
+    createdAt: String!
+    updatedAt: String!
+    title: String!
+    content: String!
+
+    authors: Group
+    recipients: Group
+
+    # La question à laquelle cette Answer répond. Non-nullable bien sûr
+    forQuestion: Question!
+}
+
+interface Request {
+    # ID de la demande
+    id: ID!
+    # message accompagnant la demande
+    message: String
+}
+
+# Demande d'un utilisateur désirant rejoindre le groupe.
+type UserJoinGroup implements Request{
+    id: ID!
+    message: String
+    # Émetteur de la demande
+    user: User
+}
+
+
+# Demande d'un groupe voulant rejoindre un événement
+type GroupJoinEvent implements Request{
+    id: ID!
+    message: String
+    # Événement concerné
+    event: Event
+    # Groupe voulant rejoindre l'événement
+    groupWantingToJoin: Group
+}
+
+# Demande au récipiendaire de rejoindre l'organisation d'un événement.
+type YourGroupHostEvent implements Request{
+    id: ID!
+    message: String
+    # Événement concerné
+    event: Event
+    # Groupe ayant publié l'évènement et lancé l'invitation
+    sender: Group
+}
diff --git a/src/ldap/admins.ts b/src/ldap/admins.ts
deleted file mode 100644
index 4b08db197a27ca14b23cc5ee90993d955c593960..0000000000000000000000000000000000000000
--- a/src/ldap/admins.ts
+++ /dev/null
@@ -1,410 +0,0 @@
-/**
- * @file Ce fichier regroupe les différentes classes avec différents admins. Ces classes sont dédiées à être exportées directement pour être utilisées par le solver.
- * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
- * @author hawkspar
- */
-
-import {ldapConfig} from './config';
-import {LDAP} from './basics';
-import {Tests} from './utilities';
-import {Open, User, userData, groupData} from './users';
-
-export class Admin extends User {
-    /**
-     * @class Cette classe est la classe de l'administrateur d'un groupe qui lui permet de rajouter des membres, en supprimer, idem pour des admins,
-     * ou éditer, voir supprimer le groupe.
-     * @summary Ce constructeur appelle simplement le constructeur de sa classe mère.
-    */
-    constructor() { super(); }
-    
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions de relation TBT
-    //------------------------------------------------------------------------------------------------------------------------
-    
-    /**
-     * @summary Fonction qui permet de rajouter un membre déjà créé à un groupe.
-     * @desc Cette fonction fait essentiellement appel à {@link LDAP.modifier} et {@link listerGroupes}. Elle n'autorise pas les doublons et opère dans les deux dns users
-     * et groups.
-     * @arg {string} uid - Identifiant du futur membre
-     * @arg {string} gid - Identifiant du groupe
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async addGroupMember(uid: string, gid: string) : Promise<boolean> {
-        try {
-            // Vérifie que l'utilisateur est pas déjà membre pour groupes
-            let lm = await Open.getMembers(gid);
-            if (!lm.includes(uid)) {
-                let vals = {};
-                vals[ldapConfig.groups.member] = uid;
-                // Erreur si pb lors de la modification
-                if (!await LDAP.change(ldapConfig.key_id+gid+ldapConfig.dn_groups, "add", vals)) {
-                    throw "Erreur lors de la modification dans l'arbre des groupes pour ajouter un membre.";
-                }
-            }
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
-        }
-        try {
-            // Vérifie que l'utilisateur est pas déjà membre pour users
-            let lg = await Open.getGroups(uid);
-            if (!lg.includes(gid)) {
-                let vals2 = {};
-                vals2[ldapConfig.users.groups] = gid;
-                // Erreur si pb lors de la modification
-                if (!await LDAP.change(ldapConfig.key_id+uid+ldapConfig.dn_users, "add", vals2)) {
-                    throw "Erreur lors de la modification dans l'arbre des utilisateurs pour ajouter un membre.";
-                }
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
-        }
-    }
-
-    /**
-     * @summary Fonction qui permet de supprimer un membre existant d'un groupe.
-     * @desc Cette fonction fait essentiellement appel à {@link LDAP.search}, {@link LDAP.change}, {@link Open.getGroups} et {@link Open.getMembers}.
-     * @arg {string} uid - Identifiant de l'ex-membre
-     * @arg {string} gid - Identifiant du groupe
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async delGroupMember(uid: string, gid: string): Promise<boolean> {
-        try {
-            // Vérifie que l'utilisateur est pas déjà viré pour groupes
-            let lm = await Open.getMembers(gid);
-            if (lm.includes(uid)) {
-                // Supprime tous les utilisateurs
-                if (!await LDAP.change(ldapConfig.key_id+gid+ldapConfig.dn_groups, "del", ldapConfig.group.member)) {
-                    throw "Erreur lors de la suppression de tous les membres du groupe.";
-                }
-                // Les rajoute un par un, sauf pour le supprimé
-                lm.forEach(id => {
-                    if (id!=uid) {
-                        this.addGroupMember(id, gid).then(res => {
-                            if (!res) { throw "Erreur lors du ré-ajout des autres membres"; }
-                        });
-                    }
-                });
-            }
-        }
-        catch(err) {
-            throw "Erreur pour obtenir une liste de membres d'un groupe pour supprimer un membre du groupe.";
-        }
-        try {
-            let lg = await Open.getGroups(uid);
-            // Vérifie que l'utilisateur est pas déjà viré pour users
-            if (lg.includes(gid)) {
-                // Supprime tous les groupes
-                if (!await LDAP.change(ldapConfig.key_id+uid+ldapConfig.dn_users, "del", ldapConfig.member.groups)) {
-                    throw "Erreur lors de la suppression de tous les groupes du membre.";
-                }
-                // Les rajoute un par un, sauf pour le supprimé
-                lg.forEach(id => {
-                    if (id!=gid) {
-                        this.addGroupMember(uid, id).then(res => {
-                            if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; }
-                        });
-                    }
-                });
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur pour obtenir une liste de groupes d'un membres pour le supprimer du groupe.";
-        }
-    }
-
-    /**
-     * @summary Fonction qui permet de promouvoir membre au stade d'administrateur d'un groupe.
-     * @desc Cette fonction fait essentiellement appel à {@link Admin.addGroupMember} {@link LDAP.change} et {@link Open.getAdmins}. Elle n'autorise pas
-     * les doublons et opère dans les deux dns users et groups.
-     * @arg {string} uid - Identifiant du futur membre
-     * @arg {string} gid - Identifiant du groupe
-     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async addGroupAdmin(uid: string, gid: string): Promise<boolean> {
-        // Ajoute le membre au groupe avant d'en faire un admin
-        if (!await Admin.addGroupMember(uid,gid)) { throw "Erreur lors de l'ajout du futur admin en tant que membre."; }
-        try {
-            let la = await Open.getAdmins(gid);
-            if (!la.includes(uid)) {
-                // Finalement modification, uniquement dans groups
-                let vals = {};
-                vals[ldapConfig.groups.admin] = uid;
-                if (!await LDAP.change(ldapConfig.key_id+gid+ldapConfig.dn_groups, "add", vals)) {
-                    throw "Erreur lors de l'ajout de l'admin dans l'arbre des groupes.";
-                }
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
-        }
-    }
-
-    /**
-     * @summary Fonction qui permet de rétrograder un membre du stade d'administrateur d'un groupe au stade d'utilisateur.
-     * @desc Cette fonction fait essentiellement appel à {@link LDAP.change}.
-     * @arg {string} uid - Identifiant du futur membre
-     * @arg {string} gid - Identifiant du groupe
-     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async delGroupAdmin(uid: string, gid: string): Promise<boolean> {
-        // Peut paraître absurde mais permet de s'assurer que le membre est bien présent et que ses champs sont comme il faut
-        if (!(await Admin.delGroupMember(uid, gid)&&Admin.addGroupMember(uid,gid))) { throw "Erreur dans l'éjection/réadmission du futur admin."; }
-        try {
-            // Vérifie que l'utilisateur est bien admin (comme dans delGroupMember)
-            let la = await Open.getAdmins(gid);
-            if (la.includes(uid)) {
-                // Supprime tous les administrateurs
-                if (!await LDAP.change(ldapConfig.key_id+gid+ldapConfig.dn_groups, "del", ldapConfig.group.admin)) { throw "Erreur dans la suppression de tous les admins pour en supprimer un."; }
-                // Les rajoute un par un, sauf pour le supprimé
-                la.forEach(id => {
-                    if (id!=uid) { Admin.addGroupAdmin(id, gid).then(res => {
-                        if (!res) { throw "Erreur dans le réajout d'un des autres admins."; }
-                    }); }
-                });
-            }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
-        }
-    }
-    
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonction d'édition TBT
-    //------------------------------------------------------------------------------------------------------------------------
-
-    /**
-     * @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 {string} gid - Identifiant du groupe à modifier
-     * @arg {groupData} data - Dictionnaire des informations utilisateurs au même format que pour {@link User.addGroup} avec tous les champs optionnels.
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async editGroup(gid: string, data: groupData) : Promise<boolean> {
-        try {
-            // Récupération des anciennes données
-            let profil = await Open.peekGroup(gid);
-            // Reecriture de profil avec les bons champs
-            Object.keys(profil).forEach(keyLDAP => {
-                Object.keys(ldapConfig.group).forEach(keyAlias => {
-                    ldapConfig.group[keyAlias]=keyLDAP;
-                    profil[keyAlias]=profil[keyLDAP];
-                });
-            });
-            // Surcharge des champs à modifier selon data
-            Object.keys(data).forEach(key => {
-                profil[key]=data[key];
-            });
-            // Modification propre
-            if (!await Admin.delGroup(gid)&&await User.addGroup(profil)) { throw "Erreur de la destruction/recréation du groupe pour le modifier."; }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention du profil d'un groupe pour le modifier.";
-        }
-    }
-    
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions de suppression TBT
-    //------------------------------------------------------------------------------------------------------------------------
-    
-    /**
-     * @summary Fonction qui supprime un groupe du LDAP.
-     * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement.
-     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant.
-     * @arg {string} gid - gid de la victime
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async delGroup(gid): Promise<boolean> {
-        try {
-            // Gestion des membres et administrateurs d'abord
-            let profil = await Open.peekGroup(gid);
-            // Ordre important
-            profil[ldapConfig.group['admin']].forEach( id => {
-                this.delGroupAdmin( id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un admin d'un groupe en cours de suppression."; } });
-            });
-            profil[ldapConfig.group['member']].forEach(id => {
-                this.delGroupMember(id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un membre."; } });
-            });
-            // Elimination
-            if (!await LDAP.clear(ldapConfig.key_id+"="+gid+","+ldapConfig.dn_groups)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; }
-            return true;
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention du profil d'un groupe pour le supprimer.";
-        }
-    }
-}
-
-export class Supervisor extends Admin {
-    /**
-     * @class Cette classe est la classe du super administrateur qui créé et supprime des membres.
-     * @summary Constructeur vide.
-     * @author hawkspar
-     */
-    constructor(user) { super(); }
-
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions de création TBT
-    //------------------------------------------------------------------------------------------------------------------------
-    
-    /**
-     * @summary Fonction qui créé un nouvel utilisateur dans le LDAP.
-     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link User.addGroupMember} et {@link Admin.addGroupAdmin} pour gérer les groupes du nouvel utilisateur.
-     * @arg {userData} data - Dictionnaire des informations utilisateurs. Des erreurs peuvent apparaître si tous les champs ne sont pas remplis.
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async addUser(data: userData): Promise<boolean> {
-        // Calcul d'un dictionnaire d'ajout
-        let vals = {};
-
-        // uid de base généré à partir de nom et prénom, plus potentiellement promo et un offset
-        // MEF mélange de Promise et de fonction standard
-        try {
-            Tests.generateUid(data['givenName'],data['lastName'],data['promotion']).then(id => { vals[ldapConfig.key_id]=id; } );
-        }
-        catch(err) {
-            throw "Erreur lors de la génération d'un hruid pour un nouvel utilisateur.";
-        }
-
-        let uid = vals[ldapConfig.key_id];
-
-        // Ecriture de toutes les valeurs directement inscrites dans le LDAP (in pour input)
-        // Génère une erreur si un champ n'est pas rempli
-        ldapConfig.user.single.forEach(key_att => vals[ldapConfig.user.single[key_att]]=data[key_att]);
-
-        // Appel à la fonction de base
-        if (!await LDAP.add(ldapConfig.key_id+"="+uid+","+ldapConfig.dn_users, vals)) { throw "Erreur de l'ajout de la feuille à l'arbre utilisateur."; }
-        
-        // Modifications multiples pour avoir plusieurs champs de même type ; boucle sur les attributs multiples
-        ldapConfig.user.multiple.forEach(key_att => {
-            // On rajoute chaque valeur en entrée
-            data[key_att].forEach(val => {
-                let vals2 = {};
-                vals2[ldapConfig.user.multiple[key_att]]=val;
-                LDAP.change(ldapConfig.key_id+"="+uid+","+ldapConfig.dn_users, "add", vals2).then(res => {
-                    if (!res) { throw "Erreur lors de l'ajout d'une valeur pour un champ à valeurs multiples à la feuille du nouvel utilisateur."; }
-                });
-            });
-        });
-
-        // Certains champs nécessitent de petits calculs
-        let vals3={};
-
-        // Création d'un nom complet lisible
-        vals3[ldapConfig.user.single['fullName']]=data['givenName']+' '+data['lastName'].toUpperCase();
-
-        // ldapConfiguration du mot de passe utilisateur
-        // Le préfixe {CRYPT} signifie que le mdp est hashé dans OpenLDAP voir : https://www.openldap.org/doc/admin24/security.html 
-        vals3[ldapConfig.user.single['password']] = "{CRYPT}"+data['password'];
-        
-        // Ecriture d'un surnom s'il y a lieu
-        if ((data['nickname']!=undefined) && (data['nickname']!='')) {
-            vals3[ldapConfig.user.single['nickname']]=data['nickname'];
-        }
-        try {
-            // Génération id aléatoire unique
-            vals3[ldapConfig.user.single['id']]= await Tests.generateId(ldapConfig.user.single['id'], ldapConfig.dn_users);
-        }
-        catch(err) {
-            throw "Erreur lors de la génération d'un id numérique pour un nouvel utilisateur.";
-        }
-        
-        // Stockage machine ; dépend du prénom
-        vals3[ldapConfig.user['directory']] = '/hosting/users/' + data['givenName'][0];
-
-        // Code root
-        vals3[ldapConfig.user.single['cleanFullName']]=data['fullName'].replace(':', ';').toLowerCase().normalize('UFD');
-        
-        // Adressage root
-        if (data['groups'].includes("on_platal")) { vals3[ldapConfig.user.single['login']] = "/bin/bash"; }
-        else  { vals3[ldapConfig.user.single['login']] = "/sbin/nologin"; }
-        
-        // Permissions BR
-        vals3[ldapConfig.user.single['readPerm']] = 'br.*,public.*';
-        if (data['readPerm'].length>0) { vals3[ldapConfig.user.single['readPerm']] += ',' + data['readPerm']; }
-        vals3[ldapConfig.user.single['writePerm']] = 'br.*,!br.blague-du-jour,public.*,!br.campagnekes';
-        if (data['writePerm'].length>0) { vals3[ldapConfig.user.single['readPerm']] += ',' + data['writePerm']; }
-
-        // Valeur nécessaire ASKIP mais inutile
-        vals3[ldapConfig.user.single['idNum']] ='5000';
-
-        // Inscription des valeurs calculées
-        if (!await LDAP.change(ldapConfig.key_id+"="+uid+","+ldapConfig.dn_users, "add", vals3)) {
-            throw "Erreur lors de l'ajout des valeurs calculées à la feuille du nouvel utilisateur.";
-        }
-
-        ["posixAccount", "shadowAccount", "inetOrgPerson", "brAccount"].forEach(cst => {
-            let val3={};
-            vals3[ldapConfig.user.multiple['class']]=cst;
-            LDAP.change(ldapConfig.key_id+"="+uid+","+ldapConfig.dn_users, "add", vals3).then(res => {
-                if (!res) { throw "Erreur lors de l'ajout d'une valeur constante à la feuille du nouvel utilisateur."; }
-            });
-        });
-
-        // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
-        data['groupsIsMember'].forEach(gid => {
-            Admin.addGroupMember(uid, gid).then(res => {
-                if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe."; }
-            });
-        });
-        data['groupsIsAdmin'].forEach(gid => { 
-            Admin.addGroupAdmin(uid, gid).then(res => {
-                if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe en tant qu'admin."; }
-            });
-        });
-
-        return true;
-    }
-
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions de suppression TBT
-    //------------------------------------------------------------------------------------------------------------------------
-    
-    /**
-     * @summary Fonction qui supprime un utilisateur du LDAP.
-     * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement.
-     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant.
-     * @arg {string} uid - uid de la victime
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async delUser(uid: string): Promise<boolean> {
-        try {
-            // Gestion des groupes d'abord
-            let profil = await Open.peekUser(uid);
-            profil[ldapConfig.user.multiple['groups']].forEach(gid => {
-                if (Open.isGroupAdmin(uid,gid)) {
-                    if (!Admin.delGroupAdmin(uid, gid)) { throw "Erreur lors de la suppression des droits d'admin de l'utilisateur."; }
-                }
-                if (!Admin.delGroupMember(uid, gid)) { throw "Erreur lors de la suppression de l'appartenance à un groupe de l'utilisateur."; }
-            });
-        }
-        catch(err) {
-            throw "Erreur lors de l'obtention des informations de l'utilisateur à supprimer.";
-        }
-        // Elimination
-        if (!LDAP.clear(ldapConfig.key_id+"="+uid+","+ldapConfig.dn_users)) { throw "Erreur lors de la suppression de l'utilisateur."; }
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/src/ldap/basics.ts b/src/ldap/basics.ts
index 9e29669db22816ed92d17484f6f213192aa79d5f..1ce31791fd8ac0d6b0cc1e9fe89337007d6225bc 100644
--- a/src/ldap/basics.ts
+++ b/src/ldap/basics.ts
@@ -76,16 +76,20 @@ export class LDAP {
      * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit les valeurs trouvées.
      * @desc Cette fonction utilise ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode search). Cette fonction fait une demande au LDAP
      * qu'elle filtre selon un schéma prédéfini dans `filter` et à chaque résultat (event SearchEntry) le met dans une liste, et renvoit la liste à l'issue (event end).
-     * @arg {string} dn - DN de l'emplacement de la requête
-     * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape
+     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
      * @arg {string[]} attributes - Liste des attributs qui figureront dans le résultat final ; peut aussi être un seul élément
+     * @arg {string} id [null] - Identifiant facultatif pour une recherche triviale en o(1)
+     * @arg {string} filter ["(objectClass=*)"] - Filtre logique de la recherche (format [`RFC2254`](https://tools.ietf.org/search/rfc2254)) déjà passé au ldapEscape
      * @return {(Promise(Array.<Object>)|Promise(Array.Object.<string, Object>))} Résultats de la recherche ; soit une liste de valeurs d'attributs, 
      * soit une liste de dictionnaires si on veut plus d'un attribut (les clés du dictionnaire sont celles du LDAP)
      * @static
      * @async
      */
-    static async search(dn: string, attributes: string[], filter="(objectClass=*)") : Promise<Array<any>> {
+    static async search(domain: 'gr'|'us', attributes: string[], id=null, filter="(objectClass=*)") : Promise<Array<any>> {
         LDAP.adminBind();
+        if (domain == "gr") { var dn = ldapConfig.dn_groups; }
+        else                { var dn = ldapConfig.dn_users; }
+        if (id != null) { dn=ldapConfig.key_id+'='+id+','+dn; }
         let vals=[];
         // Interrogation LDAP selon ldapConfiguration fournie en argument
         client.search(ldapEscape.dn("${txt}", { txt: dn}), {
@@ -112,10 +116,11 @@ 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 et on renvoit la liste
-                res.on('end', res => { LDAP.unbind(); });
+                // Si la recherche est finie on se déconnecte
+                res.on('end', _ => { LDAP.unbind(); });
             }
         });
+        // On renvoit le résultat
         return vals;
     }
 
@@ -123,8 +128,9 @@ export class LDAP {
     /**
      * @summary Fonction qui permet de modifier un élément sur le LDAP. Gestion intelligente de l'appartenance à un binet.
      * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode modify).
-     * @arg {string} dn - DN de l'endroit à 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 {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
+     * @arg {string} id - Identifiant unique de la feuille à modifier
+     * @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.
@@ -132,11 +138,14 @@ export class LDAP {
      * @static
      * @async
      */
-    static async change(dn: 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 => {
@@ -150,15 +159,17 @@ export class LDAP {
     /**
      * @summary Fonction qui permet de rajouter un élément sur le LDAP.
      * @desc  Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode add).
-     * @arg {string} dn - Adresse du parent
-     * @arg {Object.<string, string>} vals - Dictionnaire contenant les valeurs à créer
+     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
+     * @arg {Object.<string, string>} vals - Dictionnaire contenant les valeurs à créer (contient un champ en ldapConfig.key_id)
      * @arg {Object} vals[key] - Nouvelle valeur pour le champ key
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon.
      * @static
      * @async
      */
-    static async add(dn: string, vals) : Promise<boolean> {
+    static async add(domain: 'gr'|'us', vals) : Promise<boolean> {
         LDAP.adminBind();
+        if (domain == "gr") { var dn = ldapConfig.dn_groups }
+        else                { var dn = ldapConfig.dn_users }
         // Ajout LDAP selon la ldapConfiguration en argument
         client.add(ldapEscape.dn(ldapConfig.key_id+"="+vals[ldapConfig.key_id]+",${txt}", { txt: dn}), vals, err => {
             throw "Erreur lors d'une opération d'ajout sur le LDAP.";
@@ -172,13 +183,17 @@ export class LDAP {
      * @summary Fonction qui permet de supprimer une feuille du LDAP.
      * @desc Cette fonction traite la demande avec ldapjs (voir [`Client API`](http://ldapjs.org/client.html) méthode del).
      * Elle est différente de modify avec "del" car elle affecte directement une feuille et pas un attribut.
-     * @arg {string} dn - Adresse de la cible
+     * @arg {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
+     * @arg {string} id - Identifiant unique de la cible
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
      * @static
      * @async
      */
-    static async clear(dn: string) : Promise<boolean> {
+    static async clear(domain: 'gr'|'us', id: string) : Promise<boolean> {
         LDAP.adminBind();
+        let dn = ldapConfig.key_id+'='+id+','
+        if (domain == "gr") { dn+=ldapConfig.dn_groups }
+        else                { dn+=ldapConfig.dn_users }
         // Suppression LDAP
         client.del(ldapEscape.dn("${txt}", {txt: dn}), err => {
             throw "Erreur lors d'une opération de suppression sur le LDAP.";
diff --git a/src/ldap/group.ts b/src/ldap/group.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91dbaf6324cdca1a40f42888061a2e501a86a272
--- /dev/null
+++ b/src/ldap/group.ts
@@ -0,0 +1,381 @@
+/**
+ * @file Ce fichier contient la classe de l'API du LDAP qui gère les opérations sur les groupes.
+ * @author hawkspar
+ */
+
+import {ldapConfig} from './config';
+import {LDAP} from './basics';
+import {Tools} from './utilities';
+
+/**
+ * @interface groupData
+ * @var {string} gid - Identifiant du groupe
+ * @var {string} name - Nom du groupe
+ * @var {string} type - Statut du groupe ; binet, section sportive... (actuellement juste 'binet' ou 'free')
+ * @var {string[]} members - Liste des membres du groupe
+ * @var {string[]} admins - Liste des admins du groupe ; supposée être une sous-liste de la précédente
+ * @var {string} description - Description du groupe (facultatif)
+ */
+export interface groupData {
+    "gid": string,
+	"name": string,
+	"type": string,
+    "members": string[],
+    "admins": string[],
+    "description"?: string
+}
+
+//------------------------------------------------------------------------------------------------------------------------
+// Classes à exporter TBT
+//------------------------------------------------------------------------------------------------------------------------
+
+export class Group {
+    /**
+     * @class Cette classe est une des deux classes exportables permettant de faire des opérations sur les groupes.
+     * @summary Constructeur vide.
+    */
+    constructor() {}
+     
+    /**
+     * @summary Fonction qui renvoit toutes les infos relatives à un groupe particulier.
+     * @desc Cette fonction utilise {@link Tools.genericPeek} avec l'interface {@link groupData}.
+     * @arg {string} gid - Identifiant du groupe
+     * @return {Promise(groupData)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ;
+     * voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes.
+     * @static
+     * @async
+     */
+    static async peek(gid: string) : Promise<groupData> {
+        try {
+            return Tools.genericPeek<groupData>("gr", gid);
+        }
+        catch(err) {
+            throw "Erreur lors d'une recherche d'informations sur un groupe.";
+        }
+    }
+
+    /**
+     * @summary Fonction qui retrouve le groupe qui ressemblent à l'input et qui correspond au type fourni. Etape 0 vers un vrai TOL (Trombino On Line).
+     * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. 
+     * Accepte des champs exacts ou incomplets mais pas approximatifs
+     * et ne gère pas l'auto-complete. Cette fonction utilise aussi ldapConfig.json. MEF Timeout pour
+     * des recherches trop vagues. Renvoit une liste d'uid.
+     * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe.
+     * @return {Promise(string[])} Liste des gid dont le nom ressemble à l'input.
+     * @static
+     * @async
+    */
+    static async search(input: string) : Promise<string[]> {
+        try {
+            return Tools.genericSearch("gr", {
+                "gid": input,
+                "name": "",
+                "type": "",
+                "members": [],
+                "admins": []
+            });
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche approximative d'un groupe.";
+        }
+    }
+
+    static async addMember(uid: string, gid: string) : Promise<boolean> {
+        try {
+            // Vérifie que l'utilisateur est pas déjà membre pour groupes
+            let lm = await Tools.getMembers(gid);
+            if (!lm.includes(uid)) {
+                let vals = {};
+                vals[ldapConfig.group.members] = uid;
+                // Erreur si pb lors de la modification
+                if (!await LDAP.change("gr", gid, "add", vals)) {
+                    throw "Erreur lors de la modification dans l'arbre des groupes pour ajouter un membre.";
+                }
+            }
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
+        }
+        try {
+            // Vérifie que l'utilisateur est pas déjà membre pour users
+            let lg = await Tools.getGroups(uid);
+            if (!lg.includes(gid)) {
+                let vals2 = {};
+                vals2[ldapConfig.user.groups] = gid;
+                // Erreur si pb lors de la modification
+                if (!await LDAP.change("us", uid, "add", vals2)) {
+                    throw "Erreur lors de la modification dans l'arbre des utilisateurs pour ajouter un membre.";
+                }
+            }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche de la liste des membres pour ajouter un membre.";
+        }
+    }
+
+    /**
+     * @summary Fonction qui permet de supprimer un membre existant d'un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link LDAP.search}, {@link LDAP.change}, {@link Open.getGroups} et {@link Open.getMembers}.
+     * @arg {string} uid - Identifiant de l'ex-membre
+     * @arg {string} gid - Identifiant du groupe
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async remMember(uid: string, gid: string): Promise<boolean> {
+        try {
+            // Vérifie que l'utilisateur est pas déjà viré pour groupes
+            let lm = await Tools.getMembers(gid);
+            if (lm.includes(uid)) {
+                // Supprime tous les utilisateurs
+                if (!await LDAP.change("gr", gid, "del", ldapConfig.group.members)) {
+                    throw "Erreur lors de la suppression de tous les membres du groupe.";
+                }
+                // Les rajoute un par un, sauf pour le supprimé
+                lm.forEach(id => {
+                    if (id!=uid) {
+                        this.addMember(id, gid).then(res => {
+                            if (!res) { throw "Erreur lors du ré-ajout des autres membres"; }
+                        });
+                    }
+                });
+            }
+        }
+        catch(err) {
+            throw "Erreur pour obtenir une liste de membres d'un groupe pour supprimer un membre du groupe.";
+        }
+        try {
+            let lg = await Tools.getGroups(uid);
+            // Vérifie que l'utilisateur est pas déjà viré pour users
+            if (lg.includes(gid)) {
+                // Supprime tous les groupes
+                if (!await LDAP.change("us", uid, "del", ldapConfig.user.groups)) {
+                    throw "Erreur lors de la suppression de tous les groupes du membre.";
+                }
+                // Les rajoute un par un, sauf pour le supprimé
+                lg.forEach(id => {
+                    if (id!=gid) {
+                        this.addMember(uid, id).then(res => {
+                            if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; }
+                        });
+                    }
+                });
+            }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur pour obtenir une liste de groupes d'un membres pour le supprimer du groupe.";
+        }
+    }
+
+    /**
+     * @summary Fonction qui permet de promouvoir membre au stade d'administrateur d'un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Admin.addGroupMember} {@link LDAP.change} et {@link Open.getAdmins}. Elle n'autorise pas
+     * les doublons et opère dans les deux dns users et groups.
+     * @arg {string} uid - Identifiant du futur membre
+     * @arg {string} gid - Identifiant du groupe
+     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async addAdmin(uid: string, gid: string): Promise<boolean> {
+        // Ajoute le membre au groupe avant d'en faire un admin
+        if (!await Group.addMember(uid,gid)) { throw "Erreur lors de l'ajout du futur admin en tant que membre."; }
+        try {
+            let la = await Tools.getAdmins(gid);
+            if (!la.includes(uid)) {
+                // Finalement modification, uniquement dans groups
+                let vals = {};
+                vals[ldapConfig.group.admins] = uid;
+                if (!await LDAP.change("gr", gid, "add", vals)) {
+                    throw "Erreur lors de l'ajout de l'admin dans l'arbre des groupes.";
+                }
+            }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
+        }
+    }
+
+    /**
+     * @summary Fonction qui permet de rétrograder un membre du stade d'administrateur d'un groupe au stade d'utilisateur.
+     * @desc Cette fonction fait essentiellement appel à {@link LDAP.change}.
+     * @arg {string} uid - Identifiant du futur membre
+     * @arg {string} gid - Identifiant du groupe
+     * @return {boolean} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async remAdmin(uid: string, gid: string): Promise<boolean> {
+        // Peut paraître absurde mais permet de s'assurer que le membre est bien présent et que ses champs sont comme il faut
+        if (!(await Group.remMember(uid, gid) && Group.addMember(uid,gid))) { throw "Erreur dans l'éjection/réadmission du futur admin."; }
+        try {
+            // Vérifie que l'utilisateur est bien admin (comme dans delGroupMember)
+            let la = await Tools.getAdmins(gid);
+            if (la.includes(uid)) {
+                // Supprime tous les administrateurs
+                if (!await LDAP.change("gr", gid, "del", ldapConfig.group.admins)) { throw "Erreur dans la suppression de tous les admins pour en supprimer un."; }
+                // Les rajoute un par un, sauf pour le supprimé
+                la.forEach(id => {
+                    if (id!=uid) { Group.addAdmin(id, gid).then(res => {
+                        if (!res) { throw "Erreur dans le réajout d'un des autres admins."; }
+                    }); }
+                });
+            }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur lors de l'obtention de la liste des administrateurs d'un groupe.";
+        }
+    }
+    /**
+     * @summary Fonction qui créé un nouveau groupe dans le LDAP.
+     * @desc Cette fonction fait une utilisation massive d'eval pour anonymiser son code ; c'est mal et cela suppose que beaucoup de soins ont été pris lors de
+     * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Admin.addMemberGroup} et {@link Admin.addAdminGroup}
+     * pour gérer les groupes du nouvel utilisateur. Attention une manip FOIREUSE est cachée dedans.
+     * @arg {groupData} data - Dictionnaire des informations utilisateurs (voir détail des champs dans ldapConfig.json)
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async create(data: groupData) : Promise<boolean> {
+        // Calcul d'un dictionnaire d'ajout
+        let vals = {};
+
+        // gid de base généré à partir du nom standardisé, pas à partir de l'entrée 'gid' !
+        try {
+            Tools.generateReadableId(data['name']).then(id => {
+                vals[ldapConfig.key_id]=id;
+                vals[ldapConfig.group['name']]=id;
+            });
+        }
+        catch(err) {
+            throw "Erreur lors de la génération d'un hruid pour créer un nouveau groupe.";
+        }
+
+        let gid : string = vals[ldapConfig.key_id];
+
+        // Ecriture de toutes les valeurs directement inscrites dans le LDAP
+        for (let key_att in data) { vals[ldapConfig.group[key_att]]=data[key_att] };
+
+        // Appel à la fonction de base
+        if (!await LDAP.add("gr", vals)) {
+            throw "Erreur lors de la création d'une nouvelle feuille dans l'arbre des groupes.";
+        }
+        // Certains champs nécessitent de petits calculs
+        let vals2={};
+
+        // Encore un champ redondant
+        vals2[ldapConfig.group['adress']] = gid;
+
+        // ?!
+        vals2[ldapConfig.group['password']] = '';
+
+        // Génération id aléatoire et test contre le LDAP
+        try {
+            Tools.generateId(ldapConfig.group["idNumber"], "gr").then(id => { vals2[ldapConfig.group['idNumber']]=id; });
+        }
+        catch(err) {
+            throw "Erreur lors de la génération d'un id numérique pour créer un nouveau groupe.";
+        }
+        // FOIREUX : Hypothèse sur la structure du reste des données mais évite un test.assurerUnicite à deux variables
+        vals2[ldapConfig.group['idNumber2']]=vals2[ldapConfig.group['idNumber']];
+        
+        // Stockage machine ; dépend du prénom
+        vals2[ldapConfig.group['directory']] = '/hosting/groups/'+gid;
+
+        // Code root
+        vals2[ldapConfig.group['cleanFullName']]=data['name'].replace(':', ';').toLowerCase().normalize('UFD');
+        
+        // Adressage root
+        vals2[ldapConfig.group['login']] = "/sbin/nologin";
+        
+        // Permissions BR
+        vals2[ldapConfig.group['readPerm']] = '!*';
+        vals2[ldapConfig.group['writePerm']] = '!*';
+
+        // Inscription des valeurs calculées par effet de bord
+        if (!await LDAP.change("gr", gid, "add", vals2)) {
+            throw "Erreur lors de l'ajout des valeurs intelligentes du nouveau groupe.";
+        }
+
+        ["posixAccount", "posixGroup", "brAccount"].forEach(cst => {
+            let vals3={};
+            vals3[ldapConfig.group['classes']]=cst;
+            LDAP.change("gr", gid, "add", vals3).then(res => {
+                if (!res) { throw "Erreur lors de l'ajout des valeurs constantes du nouveau groupe."; }
+            });
+        });
+
+        // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
+        data['members'].forEach(uid => {
+            Group.addMember(uid, gid).then(res => {
+                if (!res) { throw "Erreur de l'ajout d'un membre au groupe."; }
+            });
+        });
+        data['admins'].forEach(uid => {
+            Group.addAdmin(uid, gid).then(res => {
+                if (!res) { throw "Erreur de l'ajout d'un admin au groupe."; }
+            });
+        });
+
+        return true;
+    }
+
+    /**
+     * @summary Fonction qui supprime un groupe du LDAP.
+     * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement.
+     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant.
+     * @arg {string} gid - gid de la victime
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async delete(gid): Promise<boolean> {
+        try {
+            // Gestion des membres et administrateurs d'abord
+            let profil = await Group.peek(gid);
+            // Ordre important
+            profil[ldapConfig.group['admin']].forEach( id => {
+                Group.remAdmin( id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un admin d'un groupe en cours de suppression."; } });
+            });
+            profil[ldapConfig.group['member']].forEach(id => {
+                Group.remMember(id, gid).then(res => { if (!res) { throw "Erreur lors de la suppression d'un membre."; } });
+            });
+            // Elimination
+            if (!await LDAP.clear("gr",gid)) { throw "Erreur lors de la suppression de la feuille dans l'arbre des groupes."; }
+            return true;
+        }
+        catch(err) {
+            throw "Erreur lors de l'obtention du profil d'un groupe pour le supprimer.";
+        }
+    }
+
+    /**
+     * @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 {
+            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.";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/ldap/user.ts b/src/ldap/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..35fadd5bdc3d6d3cd18dde759ea7ad83a5a1fdc4
--- /dev/null
+++ b/src/ldap/user.ts
@@ -0,0 +1,283 @@
+/**
+ * @file Ce fichier regroupe les différentes classes avec différents utilisateurs. Ces classes sont dédiées à être exportées directement pour être utilisées par le solver.
+ * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
+ * @author hawkspar
+ */
+
+import {ldapConfig} from './config';
+import {LDAP} from './basics';
+import {Tools} from './utilities';
+import {Group} from './group';
+
+/**
+ * @interface userData
+ * @desc Interface avec toutes les données extractables pour un utilisateur.
+ * @var {string} uid - Identifiant utilisateur
+ * @var {string} givenName - Prénom
+ * @var {string} lastName - Nom
+ * @var {string} nickname - Surnom
+ * @var {string} photo - Bytestring de la photo de l'utilisateur
+ * @var {string} birthdate - Date d'anniversaire
+ * TBA @var {string} nationality - Nationalité d'origine
+ * @var {string} promotion - Année(s) de promo
+ * @var {string} phone - Numéro(s) de téléphone
+ * @var {string[]} adresses - Adresse(s)
+ * @var {string[]} mails - Adresse(s) courriel
+ * @var {string[]} groups - Un ou plusieurs groupes dont l'utilisateur est membre (inclus section sportive, binet, PA...)
+ * @var {string} password - Mot de passe généré en amont
+ * @var {string[]} ips - Adresse(s) ip
+ * @var {string} directory - Adresse soft des données utilisateurs
+ * @var {string} login - Astuce de root flemmard
+ * @arg {string} readPerm - Permissions spéciales BR
+ * @var {string} writePerm - Permissions spéciales BR
+ * @var {string[]} forlifes - Alias BR (attention le filtre .fkz n'est plus fonctionnel)
+ * @var {string[]} admins - Liste des gid dont l'utilisateur est admin ; supposé sous-liste de groups
+ * TBA @var {string[]} likes - Liste des gid dont l'utilisateur est sympathisant
+ */
+export interface userData {
+    "uid": string,
+    "groups": string[],
+    "groupsIsAdmin": string[],
+    "password"?: string,
+    "givenName"?: string,
+    "lastName"?: string,
+    "nickname"?: string,
+    "promotion"?: string,
+    "photo"?: string,
+    "birthdate"?: string,
+    //"nationality"?: string,
+    "phone"?: string,
+    "adress"?: string,
+    "mail"?: string,
+    "ips"?: string[],
+    "directory"?: string,
+    "login"?: string,
+    "readPerm"?: string,
+    "writePerm"?: string,
+    "forlifes"?: string[]
+    //"likes"?: string[]
+}
+
+//------------------------------------------------------------------------------------------------------------------------
+// Classes à exporter TBT
+//------------------------------------------------------------------------------------------------------------------------
+
+export class User {
+    /**
+     * @class Cette classe est une des deux classes exportables permettant de faire des opérations sur les utilisateurs.
+     * @summary Constructeur vide.
+     */
+    constructor() {}
+     
+    /**
+     * @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(userData)} Informations recueillies.
+     * @static
+     * @async
+     */
+    static async peek(uid: string) : Promise<userData> {
+        try { 
+            return Tools.genericPeek<userData>("us", uid);
+        }
+        catch(err) {
+            throw "Error while peeking a user.";
+        }
+    }
+    
+    /**
+     * @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.
+     * @arg {userData} 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
+     * 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.
+     * @static
+     * @async
+     */
+    static async search(data: userData) : Promise<string[]> {
+        try {
+            return Tools.genericSearch("us", data);
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche approximative d'un utilisateur.";
+        }
+    }
+    
+    /**
+     * @summary Fonction qui créé un nouvel utilisateur dans le LDAP.
+     * @desc Appelle {@link LDAP.add} bien sûr, mais aussi {@link User.addGroupMember} et {@link Admin.addGroupAdmin} pour gérer les groupes du nouvel utilisateur.
+     * @arg {fullUserData} data - Dictionnaire des informations utilisateurs. Des erreurs peuvent apparaître si tous les champs ne sont pas remplis.
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async create(data: userData): Promise<boolean> {
+        // Calcul d'un dictionnaire d'ajout
+        let vals = {};
+
+        // uid de base généré à partir de nom et prénom, plus potentiellement promo et un offset
+        // MEF mélange de Promise et de fonction standard
+        try {
+            Tools.generateUid(data['givenName'],data['lastName'],data['promotion']).then(id => { vals[ldapConfig.key_id]=id; } );
+        }
+        catch(err) {
+            throw "Erreur lors de la génération d'un hruid pour un nouvel utilisateur.";
+        }
+
+        let uid = vals[ldapConfig.key_id];
+
+        // Génère une erreur si un champ n'est pas rempli
+        for (let key_att in data) {
+            // Ecriture de toutes les valeurs uniques
+            if (!Array.isArray(data[key_att])) { vals[ldapConfig.user[key_att]]=data[key_att]; }
+        }
+
+        // Appel à la fonction de base
+        if (!await LDAP.add("us", vals)) { throw "Erreur de l'ajout de la feuille à l'arbre utilisateur."; }
+        
+        for (let key_att in data) {
+            // Modifications multiples pour avoir plusieurs champs de même type ; boucle sur les attributs multiples
+            if (Array.isArray(data[key_att])) {
+                // On rajoute chaque valeur en entrée
+                data[key_att].forEach(val => {
+                    let vals2 = {};
+                    vals2[ldapConfig.user[key_att]]=val;
+                    LDAP.change("us", uid, "add", vals2).then(res => {
+                        if (!res) { throw "Erreur lors de l'ajout d'une valeur pour un champ à valeurs multiples à la feuille du nouvel utilisateur."; }
+                    });
+                });
+            }
+        }
+        
+        // Certains champs nécessitent de petits calculs
+        let vals3={};
+
+        // Création d'un nom complet lisible
+        vals3[ldapConfig.user['fullName']]=data['givenName']+' '+data['lastName'].toUpperCase();
+
+        // ldapConfiguration du mot de passe utilisateur
+        // Le préfixe {CRYPT} signifie que le mdp est hashé dans OpenLDAP voir : https://www.openldap.org/doc/admin24/security.html 
+        vals3[ldapConfig.user['password']] = "{CRYPT}"+data['password'];
+        
+        // Ecriture d'un surnom s'il y a lieu
+        if ((data['nickname']!=undefined) && (data['nickname']!='')) {
+            vals3[ldapConfig.user['nickname']]=data['nickname'];
+        }
+        try {
+            // Génération id aléatoire unique
+            vals3[ldapConfig.user['id']]= await Tools.generateId(ldapConfig.user['id'], "us");
+        }
+        catch(err) {
+            throw "Erreur lors de la génération d'un id numérique pour un nouvel utilisateur.";
+        }
+        
+        // Stockage machine ; dépend du prénom
+        vals3[ldapConfig.user['directory']] = '/hosting/users/' + data['givenName'][0];
+
+        // Code root
+        vals3[ldapConfig.user['cleanFullName']]=data['fullName'].replace(':', ';').toLowerCase().normalize('UFD');
+        
+        // Adressage root
+        if (data['groups'].includes("on_platal")) { vals3[ldapConfig.user['login']] = "/bin/bash"; }
+        else  { vals3[ldapConfig.user['login']] = "/sbin/nologin"; }
+        
+        // Permissions BR
+        vals3[ldapConfig.user['readPerm']] = 'br.*,public.*';
+        if (data['readPerm'].length>0) { vals3[ldapConfig.user['readPerm']] += ',' + data['readPerm']; }
+        vals3[ldapConfig.user['writePerm']] = 'br.*,!br.blague-du-jour,public.*,!br.campagnekes';
+        if (data['writePerm'].length>0) { vals3[ldapConfig.user['readPerm']] += ',' + data['writePerm']; }
+
+        // Valeur nécessaire ASKIP mais inutile
+        vals3[ldapConfig.user['idNum']] ='5000';
+
+        // Inscription des valeurs calculées
+        if (!await LDAP.change("us", uid, "add", vals3)) {
+            throw "Erreur lors de l'ajout des valeurs calculées à la feuille du nouvel utilisateur.";
+        }
+
+        ["posixAccount", "shadowAccount", "inetOrgPerson", "brAccount"].forEach(cst => {
+            let val3={};
+            vals3[ldapConfig.user['class']]=cst;
+            LDAP.change("us", uid, "add", vals3).then(res => {
+                if (!res) { throw "Erreur lors de l'ajout d'une valeur constante à la feuille du nouvel utilisateur."; }
+            });
+        });
+
+        // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
+        data['groupsIsMember'].forEach(gid => {
+            Group.addMember(uid, gid).then(res => {
+                if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe."; }
+            });
+        });
+        data['groupsIsAdmin'].forEach(gid => { 
+            Group.addAdmin(uid, gid).then(res => {
+                if (!res) { throw "Erreur lors de l'ajout du nouvel utilisateur à un groupe en tant qu'admin."; }
+            });
+        });
+
+        return true;
+    }
+
+    //------------------------------------------------------------------------------------------------------------------------
+    // Fonctions de suppression TBT
+    //------------------------------------------------------------------------------------------------------------------------
+    
+    /**
+     * @summary Fonction qui supprime un utilisateur du LDAP.
+     * @desc Cette fonction commence par gérer les groupes du membre puis le supprime entièrement.
+     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Admin.delGroupMember} et {@link Admin.delGroupAdmin} pour gérer les groupes de l'utilisateur sortant.
+     * @arg {string} uid - uid de la victime
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async delete(uid: string): Promise<boolean> {
+        try {
+            // Gestion des groupes d'abord
+            let profil = await User.peek(uid);
+            profil[ldapConfig.user['groups']].forEach(gid => {
+                // Opérations effectuées par effet de bord
+                if (Tools.isGroupAdmin(uid,gid)) {
+                    if (!Group.remAdmin(uid, gid)) { throw "Erreur lors de la suppression des droits d'admin de l'utilisateur."; }
+                }
+                if (!Group.remMember(uid, gid)) { throw "Erreur lors de la suppression de l'appartenance à un groupe de l'utilisateur."; }
+            });
+        }
+        catch(err) {
+            throw "Erreur lors de l'obtention des informations de l'utilisateur à supprimer.";
+        }
+        // Elimination
+        if (!LDAP.clear("us", uid)) { throw "Erreur lors de la suppression de l'utilisateur."; }
+        return true;
+    }
+
+    /**
+     * @summary Fonction qui édite un utilisateur existant dans le LDAP. Très similaire à {@link creerUtilisateur}
+     * @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 {
+            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.";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/ldap/users.ts b/src/ldap/users.ts
deleted file mode 100644
index f8fbab0e313d736e688dbd26b15a059c4619096e..0000000000000000000000000000000000000000
--- a/src/ldap/users.ts
+++ /dev/null
@@ -1,416 +0,0 @@
-/**
- * @file Ce fichier regroupe les différentes classes avec différents utilisateurs. Ces classes sont dédiées à être exportées directement pour être utilisées par le solver.
- * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
- * @author hawkspar
- */
-
-import { ldapConfig } from './config';
-import {LDAP} from './basics';
-import {searchUserFields, SmartSearch, Tests} from './utilities';
-import {Admin, Supervisor} from './admins';
-import ldap from 'ldapjs';
-
-/**
- * @const groupDataTemplate
- * @var {string} name - Nom du groupe
- * @var {string} ns - Statut du groupe ; 'binet' ou 'free', càd ouvert à tous
- * @var {string[]} members - Liste des membres du groupe
- * @var {string[]} admins - Liste des admins du groupe ; supposée être une sous-liste de la précédente
- */
-let groupDataTemplate = {};
-for (let key in ldapConfig.group.single)    { groupDataTemplate[key] = ""; }
-for (let key in ldapConfig.group.multiple)  { groupDataTemplate[key] = [""]; }
-
-/**
- * @type groupData
- * @summary Interface sur le modèle de {@link groupDataTemplate}
- */
-export type groupData = typeof groupDataTemplate;
-
-/**
- * @interface userData
- * @desc Interface avec toutes les données extractables pour un utilisateur.
- * @var {string} givenName - Prénom
- * @var {string} lastName - Nom
- * @var {string} nickname - Surnom
- * @var {string} photo - Bytestring de la photo de l'utilisateur
- * @var {string} birthdate - Date d'anniversaire
- * @var {string} promotion - Année(s) de promo
- * @var {string} phone - Numéro(s) de téléphone
- * @var {string} mail - Adresse(s) courriel
- * @var {string} ip - Adresse(s) ip
- * @var {string} adress - Adresse(s)
- * @var {string} groups - Un ou plusieurs groupes dont l'utilisateur est membre (inclus section sportive, binet, PA...)
- * @var {string} password - Mot de passe généré en amont
- * @arg {string} readPerm - Permissions spéciales BR
- * @var {string} writePerm - Permissions spéciales BR
- * @var {string[]} forlifes - Alias BR (attention le filtre .fkz n'est plus fonctionnel)
- * @var {string[]} groupsIsAdmin - Liste des gid dont le pax est admin ; supposé sous-liste de groups
- */
-let userDataTemplate = {};
-for (let key in ldapConfig.user.single)    { userDataTemplate[key] = ""; }
-for (let key in ldapConfig.user.multiple)  { userDataTemplate[key] = [""]; }
-
-/**
- * @type groupData
- * @summary Interface sur le modèle de {@link userDataTemplate}
- */
-export type userData = typeof userDataTemplate;
-
-//------------------------------------------------------------------------------------------------------------------------
-// Classes à exporter TBT
-//------------------------------------------------------------------------------------------------------------------------
-
-export class Open {
-    /**
-     * @class Cette classe est la classe exportable de base permettant à un utilisateur non connecté de faire des petites recherches simples.
-     * @summary Constructeur vide.
-    */
-    constructor() {}
-
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions de lecture
-    //------------------------------------------------------------------------------------------------------------------------
-    /**
-     * @summary Fonction qui retrouve les groupes dont un individu est membre.
-     * @desc Cette fonction utilise {@link LDAP.search} va directement à la feuille de l'utilisateur.
-     * @arg {string} uid - Identifiant de l'individu à interroger (le plus souvent prenom.nom, parfois l'année, supposé valide)
-     * @return {Promise(string[])} Liste des uid de groupes (noms flat des groupes) où l'id fourni est membre
-     * @static
-     * @async
-     */
-    static async getGroups(uid: string) {
-        try {
-            return LDAP.search(ldapConfig.key_id+uid+ldapConfig.dn_users, ldapConfig.user.groups)[0];
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche des groupes d'un individu.";
-        }
-    }
-    
-    /**
-     * @summary Fonction qui retrouve la liste des membres d'un groupe.
-     * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json.
-     * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule)
-     * @return {Promise(String[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes)
-     * @static
-     * @async
-     */
-    static async getMembers(gid: string) {
-        try {
-            return LDAP.search(ldapConfig.key_id+gid+ldapConfig.dn_users, ldapConfig.group.member)[0];
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche des membres d'un groupe.";
-        }
-    }
-    
-    /**
-     * @summary Fonction qui retrouve la liste des admins d'un groupe.
-     * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json.
-     * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule)
-     * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes)
-     * @static
-     * @async
-     */
-    static async getAdmins(gid: string) {
-        try {
-            return LDAP.search(ldapConfig.key_id+gid+ldapConfig.dn_users, ldapConfig.group.admin)[0];
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche des admins d'un groupe.";
-        }
-    }
-
-    /**
-     * @summary Cette fonction teste si un utilisateur est membre d'un groupe.
-     * @desc Utilise les méthodes statiques {@link open.getGroups} et {@link open.getMembers}
-     * @param {string} uid - Identifiant de l'utilisateur à tester 
-     * @param {string} gid  - Identification du groupe à tester
-     * @returns {Promise(boolean)} True si l'utilisateur est membre
-     * @static
-     * @async
-     */
-    static async isGroupMember(uid: string, gid: string) {
-        try {
-            let lg = await this.getGroups(uid);
-            let lm = await this.getMembers(gid);
-            if (lg.includes(gid) && lm.includes(uid)) {
-                return true;
-            }
-        }
-        catch(err) {
-            throw "Erreur lors du test d'appartenance à un groupe.";
-        }
-    }
-
-    /**
-     * @summary Cette fonction teste si un utilisateur est admin d'un groupe.
-     * @desc Utilise la méthode statique {@link Open.getAdmins}
-     * @param {string} uid - Identifiant de l'utilisateur à tester 
-     * @param {string} gid  - Identification du groupe à tester
-     * @returns {Promise(boolean)} True si l'utilisateur est administrateur
-     * @static
-     * @async
-     */
-    static async isGroupAdmin(uid: string, gid: string) {
-        try {
-            let la = await this.getAdmins(gid);
-            if (la.includes(uid)) {
-                return true;
-            }
-        }
-        catch(err) {
-            throw "Erreur lors du test d'appartenance au bureau d'administration un groupe.";
-        }
-    }
-     
-    /**
-     * @summary Fonction qui renvoit toutes les infos relatives à un utilisateur particulier.
-     * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
-     * @arg {string} uid - Identifiant de l'utilisateur
-     * @return {Promise(userData)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet de l'utilisateur ;
-     * voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes.
-     * @static
-     * @async
-     */
-    static async peekUser(uid: string) : Promise<userData> {
-        try {
-            let fields = [];
-            fields.push(ldapConfig.user.single.values());
-            fields.push(ldapConfig.user.multiple.values());
-            let LDAPUserData = await LDAP.search(ldapConfig.key_id+'='+uid+','+ldapConfig.dn_users, fields);
-            let cleanUserData = userDataTemplate;
-            // Rename output
-            for (let uncleanKey in LDAPUserData) {
-                for (let cleanKey in cleanUserData) {
-                    if (uncleanKey==ldapConfig.group.cleanKey) { cleanUserData[cleanKey] = LDAPUserData[uncleanKey]; }
-                }
-            }
-            return cleanUserData;
-        }
-        catch(err) {
-            throw "Erreur lors d'une recherche d'informations sur un individu.";
-        }
-    }
-     
-    /**
-     * @summary Fonction qui renvoit toutes les infos relatives à un groupe particulier.
-     * @desc Cette fonction utilise {@link LDAP.search} avec des attributs prédéfinis.
-     * @arg {string} gid - Identifiant du groupe
-     * @return {Promise(groupData)} Informations recueillies ; renvoie une liste de dictionnaire avec le profil complet du groupe ;
-     * voir `ldap_ldapConfig.json`(..\..\ldap_ldapConfig.json) pour les clés exactes.
-     * @static
-     * @async
-     */
-    static async peekGroup(gid: string) : Promise<groupData> {
-        try {
-            let fields = [];
-            fields.push(ldapConfig.user.single.values());
-            fields.push(ldapConfig.user.multiple.values());
-            let LDAPGroupData = await LDAP.search(ldapConfig.key_id+'='+gid+','+ldapConfig.dn_groups, fields);
-            let cleanGroupData=groupDataTemplate;
-            // Rename output
-            for (let uncleanKey in LDAPGroupData) {
-                for (let cleanKey in cleanGroupData) {
-                    if (uncleanKey==ldapConfig.group.cleanKey) { cleanGroupData[cleanKey] = LDAPGroupData[uncleanKey]; }
-                }
-            }
-            return cleanGroupData;
-        }
-        catch(err) {
-            throw "Erreur lors d'une recherche d'informations sur un groupe.";
-        }
-    }
-
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions de recherche
-    //------------------------------------------------------------------------------------------------------------------------
-
-    /**
-     * @summary Fonction qui retrouve le groupe qui ressemblent à l'input et qui correspond au type fourni. Etape 0 vers un vrai TOL (Trombino On Line).
-     * @desc Cette fonction utilise {@link SmartSearch.groups}.
-     * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe.
-     * @return {Promise(string[])} Liste des gid dont le nom ressemble à l'input.
-     * @static
-     * @async
-    */
-    static async findGroups(input: string) : Promise<string[]> {
-        try {
-            // Trucs intelligents faits dans ./utilities
-            return SmartSearch.groups(input, ldapConfig.key_id);
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche approximative d'un groupe.";
-        }
-    }
-
-    /**
-     * @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).
-     * @desc Cette fonction utilise {@link SmartSearch.users}.
-     * @arg {searchUserFields} data - Dictionnaire contenant les données nécessaires à {@link SmartSearch.groups}
-     * @return {Promise(string[])} gids des profils qui "match" les critères proposés.
-     * @static
-     * @async
-     */
-    static async findUsers(data: searchUserFields) : Promise<string[]> {
-        try {
-            return SmartSearch.users(data, ldapConfig.key_id);
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche approximative d'un utilisateur.";
-        }
-    }
-}
-
-export class User extends Open {
-    /**
-     * @class Cette classe est la classe de l'utilisateur connecté qui peut déjà créer un groupe et changer son profil.
-     * Techniquement, c'est la première classe qui a vraiment besoin de méthodes dynamiques dans l'arborescence, puisque c'est à partir du niveau User
-     * qu'on peut commencer à vouloir tracer les actions de l'utilisateur. 
-     * @summary Constructeur vide.
-    */
-    constructor() { super(); }
-
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonction de création TBT
-    //------------------------------------------------------------------------------------------------------------------------
-
-    /**
-     * @summary Fonction qui créé un nouveau groupe dans le LDAP.
-     * @desc Cette fonction fait une utilisation massive d'eval pour anonymiser son code ; c'est mal et cela suppose que beaucoup de soins ont été pris lors de
-     * l'escape de ses paramètres. Appelle {@link LDAP.add} et {@link LDAP.change}, mais aussi {@link Admin.addMemberGroup} et {@link Admin.addAdminGroup}
-     * pour gérer les groupes du nouvel utilisateur. Attention une manip FOIREUSE est cachée dedans.
-     * @arg {groupData} data - Dictionnaire des informations utilisateurs (voir détail des champs dans ldapConfig.json)
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async addGroup(data: groupData) : Promise<boolean> {
-        // Calcul d'un dictionnaire d'ajout
-        let vals = {};
-
-        // uid de base généré à partir du nom standardisé
-        try {
-            Tests.generateReadableId(data['name']).then(id => {
-                vals[ldapConfig.key_id]=id;
-                vals[ldapConfig.group.single['name']]=id;
-            });
-        }
-        catch(err) {
-            throw "Erreur lors de la génération d'un hruid pour créer un nouveau groupe.";
-        }
-
-        let gid = vals[ldapConfig.key_id];
-
-        // Ecriture de toutes les valeurs directement inscrites dans le LDAP (in pour input)
-        ldapConfig.group.single.forEach(key_att => vals[ldapConfig.group.single[key_att]]=data[key_att]);
-
-        // Appel à la fonction de base
-        if (!await LDAP.add(ldapConfig.key_id+"="+gid+","+ldapConfig.dn_groups, vals)) {
-            throw "Erreur lors de la création d'une nouvelle feuille dans l'arbre des groupes.";
-        }
-        // Certains champs nécessitent de petits calculs
-        let vals2={};
-
-        // ?!
-        vals2[ldapConfig.group.single['password']] = '';
-
-        // Génération id aléatoire et test contre le LDAP
-        try {
-            Tests.generateId(ldapConfig.group.single["idNumber"], ldapConfig.dn_groups).then(id => { vals2[ldapConfig.group.single['idNumber']]=id; });
-        }
-        catch(err) {
-            throw "Erreur lors de la génération d'un id numérique pour créer un nouveau groupe.";
-        }
-        // FOIREUX : Hypothèse sur la structure du reste des données mais évite un test.assurerUnicite à deux variables
-        vals2[ldapConfig.group.single['idNumber2']]=vals2[ldapConfig.group.single['idNumber']];
-        
-        // Stockage machine ; dépend du prénom
-        vals2[ldapConfig.group.single['directory']] = '/hosting/groups/'+gid;
-
-        // Code root
-        vals2[ldapConfig.group.single['cleanFullName']]=data['name'].replace(':', ';').toLowerCase().normalize('UFD');
-        
-        // Adressage root
-        vals2[ldapConfig.group.single['login']] = "/sbin/nologin";
-        
-        // Permissions BR
-        vals2[ldapConfig.group.single['readPerm']] = '!*';
-        vals2[ldapConfig.group.single['writePerm']] = '!*';
-
-        // Inscription des valeurs calculées par effet de bord
-        if (!await LDAP.change(ldapConfig.key_id+"="+gid+","+ldapConfig.dn_groups, "add", vals2)) {
-            throw "Erreur lors de l'ajout des valeurs intelligentes du nouveau groupe.";
-        }
-
-        ["posixAccount", "posixGroup", "brAccount"].forEach(cst => {
-            let vals3={};
-            vals3[ldapConfig.group.multiple['class']]=cst;
-            LDAP.change(ldapConfig.key_id+"="+gid+","+ldapConfig.dn_groups, "add", vals3).then(res => {
-                if (!res) { throw "Erreur lors de l'ajout des valeurs constantes du nouveau groupe."; }
-            });
-        });
-
-        // Utilisation des fonctions adaptées pour assurer la cohérence de l'ensemble
-        data['members'].forEach(uid => {
-            Admin.addGroupMember(uid, gid).then(res => {
-                if (!res) { throw "Erreur de l'ajout d'un membre au groupe."; }
-            });
-        });
-        data['admins'].forEach(uid => {
-            Admin.addGroupAdmin(uid, gid).then(res => {
-                if (!res) { throw "Erreur de l'ajout d'un admin au groupe."; }
-            });
-        });
-
-        return true;
-    }
-
-    //------------------------------------------------------------------------------------------------------------------------
-    // Fonctions d'édition TBT
-    //------------------------------------------------------------------------------------------------------------------------
-    
-    /**
-     * @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 {string} uid - Utilisateur à modifier (le plus souvent le même, mais root possible)
-     * @arg {userData} data - Dictionnaire des informations utilisateurs au même format que pour {@link creerUtilisateur} avec tous les champs optionnels ;
-     * 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)
-     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
-     * @async
-     * @static
-     */
-    static async editUser(uid : string, data : userData) : Promise<boolean> {
-        // Récupération des anciennes données
-        let profil = await Open.peekUser(uid);
-        try {
-            // Régénération du champ manquant dans profil
-            let lg = await Open.getGroups(uid);
-            profil['groupsIsAdmin']=[];
-            lg.forEach(gid => {
-                Open.isGroupAdmin(uid, gid).then(res => {
-                    if (res) { profil['groupsIsAdmin'].push(gid); }
-                });
-            });
-            // Surcharge des champs à modifier selon data
-            Object.keys(data).forEach(function(key: string) {
-                // Some fields the user cannot change (groups and groupsIsAdmin must be changed through addGroupMember and addGroupAdmin in Admin)
-                if (!['readPerm','writePerm','forlifes','ips','groups','groupsIsAdmin'].includes(key)) { profil[key]=data[key]; }
-            });
-            // Modification propre par effet de bord
-            if (!(await Supervisor.delUser(uid) && await Supervisor.addUser(profil))) {
-                throw "Erreur dans la destruction/création du compte.";
-            } else {
-                return true;
-            }
-        }
-        catch(err) {
-            throw "Erreur lors de la modification des groupes où un utilisateur est admin.";
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/ldap/utilities.ts b/src/ldap/utilities.ts
index 19b5606d3ab0a625e000656e11850e3518096d1f..dc47a122097126bc9301dc10a96f4ef2e4349acf 100644
--- a/src/ldap/utilities.ts
+++ b/src/ldap/utilities.ts
@@ -6,105 +6,76 @@
 
 import {ldapConfig} from './config';
 import {LDAP} from './basics';
-
-/**
- * @interface searchUserFields
- * @desc Interface permettant la recherche d'un utilisateur avec des champs incomplets. Plusieurs valeurs sont possibles pour le même champ.
- * Aucun de ces champs n'est obligatoire, mais certains de ces champs doivent être exacts pour obtenir un bon résultat.
- * @var {string|string[]} givenName - Prénom(s)
- * @var {string|string[]} lastName - Nom(s)
- * @var {string|string[]} nickname - Surnom(s)
- * @var {string|string[]} nationality - Nationalité(s) (à implémenter)
- * @var {string|string[]} promotion - Année(s) de promo
- * @var {string|string[]} phone - Numéro(s) de téléphone
- * @var {string|string[]} mail - Adresse(s) courriel
- * @var {string|string[]} ip - Adresse(s) ip
- * @var {string|string[]} adress - Adresse(s)
- * @var {string} school - Ecole d'appartenance (instable, doit être exact)
- * @var {string|string[]} groups - Un ou plusieurs groupes dont l'utilisateur est membre (doit être exact).
- * @var {string} course - PA ou autre. Doit être exact.
- */
-export interface searchUserFields {
-    givenName: string,
-    lastName: string,
-    nickname: string,
-    nationality: string,
-    promotion: string,
-    phone: string,
-    mail: string,
-    ip: string,
-    adress: string,
-    school: string,
-    groups: string[],
-    studies: string,
-    sport: string
-}
+import { userData } from './user';
+import { groupData } from './group';
 
 //------------------------------------------------------------------------------------------------------------------------
-// Fonctions de recherche
+// Fonctions intermédiaires TBT
 //------------------------------------------------------------------------------------------------------------------------
 
-export class SmartSearch {
+export class Tools {
     /**
-     * @class Cette classe contient des fonctions de recherche génériques trop puissantes pour être exportées tel quel.
+     * @class Cette classe contient des fonctions intermédiaires qui ne sont pas destinées à être utilisées dans les resolvers.
      * @summary Constructeur vide.
      * @author hawkspar
     */
     constructor() {}
-
+     
     /**
-     * @summary Fonction qui interroge le LDAP et retrouve les groupes (voir LDAP) qui ressemblent
-     *  à l'entrée. Etape 0 vers un vrai TOL (Trombino On Line).
-     * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. 
-     * Accepte des champs exacts ou incomplets mais pas approximatifs
-     * et ne gère pas l'auto-complete. Cette fonction utilise aussi ldapConfig.json. MEF Timeout pour
-     * des recherches trop vagues. Renvoit une liste d'uid.
-     * Elle utilise LDAPEscape pour éviter les injections.
-     * @arg {string} input - String entré par l'utilisateur qui ressemble au nom du groupe.
-     * @arg {string[]} return_attributes - Liste d'attributs à renvoyer dans le résultat final
-     * @return {Promise(string[])} Liste des uid de groupes dont le nom ressemble à l'input
+     * @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.
+     * @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 groups(input: string, return_attributes: string[]) : Promise<string[]> {
-        // Construction du filtre custom
-        let filter= "(|("+ldapConfig.key_id+"="+ input+")" +    // On cherche la valeur exacte
-                    "(|("+ldapConfig.key_id+"=*"+input+")" +    // La valeur finale avec des trucs avant ; wildcard *
-                    "(|("+ldapConfig.key_id+"=*"+input+"*)"+    // La valeur du milieu avec des trucs avant et après
-                    "("+  ldapConfig.key_id+"="+ input+"*))))"; // La valeur du début avec des trucs après
-
-        // Appel rechercheLDAP avec filtre de l'espace 
-        try {
-            return LDAP.search(ldapConfig.dn_groups, return_attributes, filter);
+    static async genericPeek<T>(domain: 'us'|'gr', id: string) : Promise<T> {
+        if (domain='gr') {
+            var dirtyKeys = ldapConfig.group;
         }
-        catch(err) {
-            throw "Erreur lors de la recherche intelligente d'un groupe.";
+        else {
+            var dirtyKeys = ldapConfig.user;
+        }
+        let cleanData : T;
+        let dirtyData = await LDAP.search(domain, dirtyKeys.values(), id);
+        // Rename output
+        for (let uncleanKey in dirtyData) {
+            for (let cleanKey in cleanData) {
+                if (uncleanKey=dirtyKeys[cleanKey]) { cleanData[cleanKey] = dirtyData[uncleanKey]; }
+            }
         }
+        return cleanData;
     }
 
+    
     /**
-     * @summary Fonction qui renvoit les attributs demandés des paxs validant les critères de recherche. Première étape vers vrai TOL (Trombino On Line).
+     * @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. Elle utilise LDAPEscape pour éviter les injections.
-     * Utiliser trouverGroupesParTypes pour chaque champ relié à groups.
-     * @arg {searchUserFields} 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
+     * 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.
+     * @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.
-     * @arg {string[]} return_attributes - Liste d'attributs à renvoyer dans le résultat final.
-     * @return {Promise(Object[])} Liste de dictionnaires de profils en cohérence avec l'input avec pour clés les attributs des profils.
+     * @return {Promise(string[])} ids des profils qui "match" les critères proposés.
      * @static
      * @async
      */
-    static async users(data: searchUserFields, return_attributes: string[]) : 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 = ldapConfig.user[key];
+                    let attribute = "";
+                    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
                                         "(|("+attribute+"=*"+val+")"+      // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs)
@@ -114,26 +85,36 @@ export class SmartSearch {
             }
         }
         // Appel avec filtre de l'espace 
-        try {
-            return LDAP.search(ldapConfig.dn_users, return_attributes, filter);
+        return LDAP.search(domain, [ldapConfig.key_id], null, filter);
+    }
+    
+    /**
+     * @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(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> {
+        if (domain = "us") {
+            var id=data['uid'];
+            var dirtyKeys=ldapConfig.user;
         }
-        catch(err) {
-            throw "Erreur lors de la recherche intelligente d'un utilisateur.";
+        else {
+            var id=data['gid'];
+            var dirtyKeys=ldapConfig.group;
         }
+        // Renommage LDAP-friendly
+        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",dirtyData);
     }
-}
-
-//------------------------------------------------------------------------------------------------------------------------
-// Fonctions intermédiaires TBT
-//------------------------------------------------------------------------------------------------------------------------
-
-export class Tests {
-    /**
-     * @class Cette classe contient des fonctions de test d'unicité trop puissantes pour être exportées tel quel.
-     * @summary Constructeur vide.
-     * @author hawkspar
-    */
-    constructor() {}
 
     /**
      * @callback changeValueCallback
@@ -148,7 +129,7 @@ export class Tests {
      * dans le dn fourni.
      * @param {string} value - Valeur de l'attribut (le plus souvent un identifiant) à tester à cette itération
      * @param {string} attribute - Attribut à tester
-     * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique
+     * @param {"gr"|"us"} domain - Domaine dans lequel l'attribut doit être unique
      * @param {changeValueCallback} changeValue - Fonction qui prend uniquement en argument l'id courant et 
      * le nombre d'itérations et qui renvoit la prochaine valeur de l'attribut 
      * @param {int} n [0] - Nombre d'itérations (à initialiser à 0)
@@ -156,15 +137,15 @@ export class Tests {
      * @static
      * @async
      */
-    static async ensureUnique(value: string, attribute: string, dn: string, changeValue: (string, number) => string, n=0) : Promise<string> {
+    static async ensureUnique(value: string, attribute: string, domain: 'gr'|'us', changeValue: (string, number) => string, n=0) : Promise<string> {
         // Recherche d'autres occurences de l'id
         try {
-            return LDAP.search(dn, ldapConfig.key_id, "("+attribute+"="+value+")").then(function (matches: string[]) {
+            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 Tests.ensureUnique(changeValue(value, n+1), attribute, dn, changeValue, n+1); }
+                else { return Tools.ensureUnique(changeValue(value, n+1), attribute, domain, changeValue, n+1); }
             });
         }
         catch(err) {
@@ -174,7 +155,7 @@ export class Tests {
 
     /**
      * @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique.
-     * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
+     * @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
      * @param {string} givenName - Prénom
      * @param {string} lastName - Nom
      * @param {string} promotion - Année de promotion
@@ -185,9 +166,9 @@ export class Tests {
     static async generateUid(givenName: string, lastName: string, promotion: string) : Promise<string> {
         try {
             // normalize et lowerCase standardisent le format
-            return this.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, ldapConfig.dn_users, (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
+            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
                 else if (n>2) { id+=n; }                        // Ensuite on continue .123, .1234, etc...
                 return id;
             });
@@ -199,7 +180,7 @@ export class Tests {
 
     /**
      * @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique.
-     * @desc Limité à un appel à {@link Tests.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
+     * @desc Limité à un appel à {@link Tools.ensureUnique} avec les bons paramètres, et quelques opérations sur l'uid pour qu'il soit valide (escape, normalisation).
      * @param {string} name - Nom
      * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
      * @static
@@ -208,9 +189,9 @@ export class Tests {
     static async generateReadableId(name: string) : Promise<string> {
         try {
             // normalize et lowerCase standardisent le format
-            return this.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, ldapConfig.dn_groups, (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...
+            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...
                 return id;
             });
         }
@@ -222,17 +203,112 @@ export class Tests {
     /**
      * @summary Cette fonction teste une valeur dummy (0) pour un identifiant numérique puis le fait évoluer aléatoirement (entre 1 et 100 000) jusqu'à ce qu'il soit unique.
      * @param {string} attribut - Intitulé exact de l'id concerné
-     * @param {string} dn - *Domain Name* dans lequel l'attribut doit être unique
+     * @param {"gr"|"us"} domain - Domaine dans lequel l'attribut doit être unique
      * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
      * @static
      * @async
      */
-    static async generateId(attribut: string, dn: string) : Promise<string> {
+    static async generateId(attribut: string, domain: "gr"|"us") : Promise<string> {
         try {
-            return this.ensureUnique("0", attribut, dn, (id,n) => { return Math.floor((Math.random() * 100000) + 1).toString(); });
+            return Tools.ensureUnique("0", attribut, domain, (id,n) => { return Math.floor((Math.random() * 100000) + 1).toString(); });
         }
         catch(err) {
             throw "Erreur lors de l'assurance de l'unicité d'un unique identifier numérique.";
         }
     }
+
+    /**
+     * @summary Fonction qui retrouve les groupes dont un individu est membre.
+     * @desc Cette fonction utilise {@link LDAP.search} va directement à la feuille de l'utilisateur.
+     * @arg {string} uid - Identifiant de l'individu à interroger (le plus souvent prenom.nom, parfois l'année, supposé valide)
+     * @return {Promise(string[])} Liste des uid de groupes (noms flat des groupes) où l'id fourni est membre
+     * @static
+     * @async
+     */
+    static async getGroups(uid: string) : Promise<string[]> {
+        try {
+            return LDAP.search("us", [ldapConfig.user.groups], uid)[0];
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche des groupes d'un individu.";
+        }
+    }
+    
+    /**
+     * @summary Fonction qui retrouve la liste des membres d'un groupe.
+     * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json.
+     * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule)
+     * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes)
+     * @static
+     * @async
+     */
+    static async getMembers(gid: string) : Promise<string[]> {
+        try {
+            return LDAP.search("gr", [ldapConfig.group.members], gid)[0];
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche des membres d'un groupe.";
+        }
+    }
+    
+    /**
+     * @summary Fonction qui retrouve la liste des admins d'un groupe.
+     * @desc Cette fonction utilise {@link LDAP.search} avec un dictionnaire prédéfini dans ldapConfig.json.
+     * @arg {string} gid - Identifiant du groupe à interroger (le plus souvent nom du groupe en minuscule)
+     * @return {Promise(string[])} Liste des uid des membres où l'id fournie est membre (noms flat des groupes)
+     * @static
+     * @async
+     */
+    static async getAdmins(gid: string) : Promise<string[]> {
+        try {
+            return LDAP.search("gr", [ldapConfig.group.admins], gid)[0];
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche des admins d'un groupe.";
+        }
+    }
+
+    /**
+     * @summary Cette fonction teste si un utilisateur est membre d'un groupe.
+     * @desc Utilise les méthodes statiques {@link open.getGroups} et {@link open.getMembers}
+     * @param {string} uid - Identifiant de l'utilisateur à tester 
+     * @param {string} gid  - Identification du groupe à tester
+     * @returns {Promise(boolean)} True si l'utilisateur est membre
+     * @static
+     * @async
+     */
+    static async isGroupMember(uid: string, gid: string) : Promise<boolean> {
+        try {
+            let lg = await Tools.getGroups(uid);
+            let lm = await Tools.getMembers(gid);
+            if (lg.includes(gid) && lm.includes(uid)) {
+                return true;
+            }
+            else { return false; }
+        }
+        catch(err) {
+            throw "Erreur lors du test d'appartenance à un groupe.";
+        }
+    }
+
+    /**
+     * @summary Cette fonction teste si un utilisateur est admin d'un groupe.
+     * @desc Utilise la méthode statique {@link Open.getAdmins}
+     * @param {string} uid - Identifiant de l'utilisateur à tester 
+     * @param {string} gid  - Identification du groupe à tester
+     * @returns {Promise(boolean)} True si l'utilisateur est administrateur
+     * @static
+     * @async
+     */
+    static async isGroupAdmin(uid: string, gid: string) : Promise<boolean> {
+        try {
+            let lm = await Tools.getMembers(gid);
+            let la = await Tools.getAdmins(gid);
+            if (la.includes(uid) && lm.includes(uid)) { return true; }
+            else { return false; }
+        }
+        catch(err) {
+            throw "Erreur lors du test d'appartenance au bureau d'administration un groupe.";
+        }
+    }
 }
\ No newline at end of file