diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 19104428c170ed62adfafd16e94b50bb36b4d15d..f8f76d23262f54e04c391d16d62e9b1b418e4976 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,15 +19,28 @@ Pour `./db/migrations`, il faut utiliser migrations_v3 (la dernière) (c'est log
 
 - une branche où on fait des petits commits pour merge dans stable
 
-### redo-connectors
+### *reconverge_ldaprefacto_redoconnectors*
+
+- faire reconverger toutes les branches, afin de s'éviter de mauvaises surprises au moment de remettre en commun (e.g. éviter que la branche implémentant les resolvers n'ait pas pris en compte une modification dans le schéma GraphQL désiré) :
+  - master, sur lequel ont eu lieu des modifs de l'authentification, de la doc et du schéma GraphQL
+  - *LDAP_refacto*, sur lequel hawkspar a écrit le code pour les fonctions d'interaction avec le LDAP (dans `src/ldap`)
+  - *redo-connectors*, où akka vodol était en train d'écrire des resolvers pour le nouveau schéma propre "_wish_list" (de kadabra).
+
+Actuellement cette branche comporte un certain nombre d'incohérences : des resolvers qui n'ont pas pris en compte des changements dans les fonctions de manipulation du LDAP, ou qui n'ont pas pris en compte des modifications du schéma GraphQL.
+
+### Anciennes branches
+
+Certaines anciennes branches, fraîchement mergées, sont décrites ci-dessous. Cette section pourra être retirée quand le merge sera proprifié.
+
+#### redo-connectors
 
 - branche sur laquelle akka vodol fait tout son travail : implémentation des resolvers pour le nouveau schéma propre (le schéma "_wish_list"). Comme ça, il peut faire d'éventuelles modifications du schéma sans faire chier les autres, et ensuite on valide ensemble pour merge.
 
-### LDAP-refacto
+#### LDAP-refacto
 
 - TODO (@hawkspar tu peux expliquer ici a quoi sert cette branche  ? et le cas échéant, la merge dans master ?)
 
-### unit-tests
+#### unit-tests
 
 - TODO (@akka vodol tu peux expliquer ici a quoi sert cette branche ? et le cas échéant, la merge dans master ?)
 
@@ -73,9 +86,7 @@ Les dossiers à la racine du projet :
     - `resolvers/` : (@akka vodol tu peux expliquer ici ?)
   - [`ldap`](./src/ldap) : gestion des requêtes au LDAP. Fournit une couche d'abstraction permettant aux resolvers GraphQL de ne pas se soucier des spécifités et de la complexité syntaxique des requêtes LDAP
 
-
 ## Base de données
-
 Sigma s'appuie sur deux bases de données (toutes deux hébergées par le BR) :
 - Le LDAP frankiz, qui contient identifiants, mots de passe, et toutes les informations sur les utilisateurs et groupes spécifiques X.
   - On y accède par des requêtes LDAP, d'où la nécessité de ldap.js (une dépendance npm faisant office de traducteur javascript/LDAP).
@@ -92,13 +103,11 @@ Cette structure peut sembler lourde et redondante mais s'explique par plusieurs
   - Le LDAP contient des informations plus sensibles (c'est là que toutes les données sur les utilisateurs sont stockées)
 
 ### Utiliser la BDD sigma avec *Knex.js*
-
 cf. [le memo knexjs](./memo\ knexjs.md).
 
 Le knexfile.js et le knex_router.js sont dans `./db`. La localisation des dossiers [seeds](./db/seeds) et [migrations](./db/migrations), est spécifiée dans le knexfile.js, en l'occurrence également dans `./db`.
 
 ### Interagir directement avec la BDD sigma en *PostgreSQL*
-
 cf. [le memo postgresql](./memo\ postgresql.md).
 
 Pour accéder à la "vraie" BDD, sur roued (le serveur qui héberge sigma), il faut
@@ -109,11 +118,9 @@ Pour accéder à la "vraie" BDD, sur roued (le serveur qui héberge sigma), il f
 - faire les requêtes en SQL (version PostgreSQL) par l'interface ainsi `psql`
 
 ### Fonctions LDAP
-
 On peut facilement explorer le LDAP si on est sur le réseau de l'X, avec [JXplorer](http://jxplorer.org/) en entrant `frankiz` dans nouvelle connexion et en allant dans `net`.
 
 ### Ne pas se tromper de LDAP...
-
 Il y a deux LDAP à l'X : le LDAP de la DSI et le LDAP de Frankiz (du BR). 
 
 - Le premier, utilisé pour l'authentification sur les services de l'Ecole (mail polytechnique.edu, le Moodle, Synapses...) ne concerne pas du tout sigma.
diff --git a/README.md b/README.md
index 0252299cfd73570d02d228f8dc7d0bd650921dd5..65bc8e0d93f14328bb60376968f4688df8484fe4 100644
--- a/README.md
+++ b/README.md
@@ -16,24 +16,46 @@ git clone https://gitlab.binets.fr/br/sigma-backend.git
 
 ## Démarrer le serveur (en mode développement)
 
-### Installer les dépendances
-Installer les dépendances (spécifiées dans `package.json`) :
+### Installer les dépendances *npm*
+On utilise un serveur node.js avec [express.js](https://expressjs.com/). [^server]
+Utiliser Node.js permet d'utiliser facilement [*npm*](https://www.npmjs.com/) (Node Package Manager). Une "dépendance" est un package utilisé dans le projet.
+
+[^server]: il est configuré dans [`app.ts`](./src/app.ts) puis lancé sur le port 3000 dans [`index.ts`](./src/index.ts).
+
+On trouve la liste des dépendances dans [`package.json`](./package.json). Express est un exemple de dépendance normale, utilisée en production ; nodemon et ESLint (voir infra) sont des dépendances dev (`devDependencies`), utilisées seulement en mode développement.
+
+Les dépendances s'installent avec `npm install`. Cette commande a deux comportements possibles selon la valeur de la variable `NODE_ENV` (vérifier avec la commande `echo "$NODE_ENV"`):
+* si `NODE_ENV` n'est pas configuré : on installe tout
+* si `NODE_ENV` == `development` : on installe tout
+* si `NODE_ENV` == `production` : on n'installe pas les dépendances développeur
+
+Pour installer les dépendances spécifiées dans `package.json` il faut donc lancer :
 ```bash
 npm install
 ```
-Certaines dépendances doivent être installées globalement :
+
+Certaines d'entre elles, comme KnexJS ou Webpack, *doivent être installées globalement* :
 ```bash
 npm install -g knex
 npm install -g webpack
 npm install -g eslint
 ```
 
-Installer PostgreSQL :
+Les dépendances principales utilisées sont 
+- *knex.js*, qui permet de construire des requêtes SQL facilement,
+- *GraphQL*, qui fournit une couche d'abstraction pour l'échange de données frontend-backend,
+- *ldap.js*, qui permet d'interroger un serveur LDAP, 
+- *webpack*, qui compile et optimise tout le code source javascript en un `bundle.js`,
+- *ESlint*, pour le développement, outil de vérification syntaxique.
+
+Et une dépendance supplémentaire, PostgreSQL (linux est supposé) :
 ```bash
 sudo apt install postgresql
 ```
 
 ### Setup la BDD PostgreSQL
+La BDD PostgreSQL est utilisée pour stocker permissions, écoles des utilisateurs, annonces et événements.
+
 Créer un rôle PostgreSQL "sigma" :
 ```bash
 sudo -u postgres -s
@@ -58,6 +80,8 @@ knex migrate:latest
 knex seed:run
 ```
 
+Voilà, vous avez une base de données à jour !
+
 ### Démarrer le serveur
 Dire à webpack de build le projet (build le bundle `./build/bundle.js`) :
 ```bash
@@ -70,10 +94,9 @@ Lancer un serveur express/node :
 ```bash
 npm run start # ou le raccourci: npm start
 ```
-Comme indiqué dans src/index.js, ceci lance un serveur servant l'application express sur le port 3000.
-
-## Déployer dans un conteneur Docker
+Comme indiqué dans src/index.js, ceci lance un serveur servant l'application express sur le port 3000. Ce serveur va ensuite éxecuter le reste du code comme si il était déployé ou presque.
 
+## Alternative : déployer dans un conteneur Docker
 L'image Docker est définie dans [`Dockerfile`](./Dockerfile). Il s'agit d'une distro Alpine avec Node.js et libstdc++. Lors du _build_ les dépendances _runtime_ dont dépend le `bundle.js` sont installées.
 
 Compiler l'image :
@@ -90,40 +113,10 @@ docker run -e LDAP_URI=ldap://172.17.0.1:8389 sigma-api
 ```
 
 ## Mode développement / staging / production
-
 TODO
 Ca a un rapport avec NODE_ENV ?
 
-## Dépendances *npm*
-
-On utilise un serveur node.js avec [express.js](https://expressjs.com/). [^server]
-Utiliser Node.js permet d'utiliser facilement [*npm*](https://www.npmjs.com/) (Node Package Manager). Une "dépendance" est un package utilisé dans le projet.
-
-[^server]: il est configuré dans [`app.ts`](./src/app.ts) puis lancé sur le port 3000 dans [`index.ts`](./src/index.ts).
-
-On trouve la liste des dépendances dans [`package.json`](./package.json). Express est un exemple de dépendance normale, utilisée en production ; nodemon et ESLint (voir infra) sont des dépendances dev (`devDependencies`), utilisées seulement en mode développement.
-
-Les dépendances s'installent avec `npm install`. Cette commande a deux comportements possibles selon la valeur de la variable `NODE_ENV` (vérifier avec la commande `echo "$NODE_ENV"`):
-* si `NODE_ENV` n'est pas configuré : on installe tout
-* si `NODE_ENV` == `development` : on installe tout
-* si `NODE_ENV` == `production` : on n'installe pas les dépendances développeur
-
-Certaines d'entre elles, comme KnexJS ou Webpack, *doivent être installées globalement* :
-```bash
-npm install -g knex
-npm install -g webpack
-npm install -g eslint
-```
-
-Les dépendances principales utilisées sont 
-- *knex.js*, qui permet de construire des requêtes SQL facilement,
-- *GraphQL*, qui fournit une couche d'abstraction pour l'échange de données frontend-backend,
-- *ldap.js*, qui permet d'interroger un serveur LDAP, 
-- *webpack*, qui compile et optimise tout le code source javascript en un `bundle.js`,
-- *ESlint*, pour le développement, outil de vérification syntaxique.
-
 ## Scripts
-
 Les scripts sont des instructions en ligne de commande que l'on peut faire tourner avec la commande `npm run`. Ce sont des raccourcis pour gagner du temps sur des opérations un peu longues. Ils sont définis dans [`package.json`](./package.json).
 
 Les plus importants sont détaillées ci-dessous :
@@ -139,13 +132,11 @@ Les plus importants sont détaillées ci-dessous :
 Donc, lancer `npm run watch` dans un terminal et `npm run start` dans un autre permet de rebuilder **et** relancer automatiquement le serveur, après toute modification *du code source*.
 
 ## Configuration
-
 L'API est conçue pour êtes modulaire et pour fonctionner dans plusieurs environnements.
 
 On peut donc le configurer via des fichiers ou des variables d'environnement.
 
 ### Configuration LDAP
-
 L'API de Sigma nécessite de se connecter au LDAP Frankiz, à la fois pour obtenir des données et pour l'authentification des utilisateurs. Cela est fait à l'aide de la librairie [ldapjs](http://ldapjs.org) pour faire les requêtes au LDAP et [passportJS](http://www.passportjs.org/) pour l'authentification.
 
 * La configuration LDAP de base se situe dans [ldap_config.json](ldap_config.json).
@@ -161,13 +152,7 @@ L'API de Sigma nécessite de se connecter au LDAP Frankiz, à la fois pour obten
 
 **Exemple**
 
-Si on développe en dehors du plâtal et qu'on ouvre un proxy SSH avec _port forwarding_ du LDAP de Frankiz (<frankiz.polytechnique.fr:389>) vers <localhost:8389>, on s'y connecte en définissant
-
-```
-LDAP_URI=ldap://localhost:8389
-```
-
-soit en faisant `export LDAP_URI=...`, soit en écrivant un fichier `.env`. Le fichier `config.js` s'occupe du reste.
+Si on développe en dehors du plâtal et qu'on ouvre un proxy SSH avec _port forwarding_ du LDAP de Frankiz (<frankiz.polytechnique.fr:389>) vers <localhost:8389>, on s'y connecte en définissant : `LDAP_URI=ldap://localhost:8389`, soit en faisant `export LDAP_URI=...`, soit en écrivant un fichier `.env`. Le fichier `config.js` s'occupe du reste.
 
 ### Variables d'environnement
 
@@ -196,41 +181,49 @@ 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. 
+Il est accessible par navigateur au path [/adminview/admin](localhost:3000/adminview/admin) ; n'importe quel path devrait rediriger dessus. 
 
 L'accès y est protégé par une page d'authentification, les identifiants à utiliser sont ceux de Frankiz. 
 Le hruid (i.e. prenom.nom) de l'utilisateur doit de plus être sur une whitelist des hruid autorisés. Pour l'instant cette whitelist est hardcodée dans le code source.
 
 ### Accès direct à la BDD
-
 Le panneau d'administration sert (ou plutôt, servira à terme) à accéder directement à la BDD propre de sigma, grâce à une API REST. Autrement dit : 
-- on accède à la table `table_name` par une requête GET à `/adminview/db/table_name`'
-- et aux colonnes `columns` de cette table par une requête GET à `/adminview/db/table_name?columns=columns`.
+- on accède à la table `table_name` par une requête GET à [adminview/db/table_name](localhost:3000/adminview/db/table_name)'
+- et aux colonnes `columns` de cette table par une requête GET à [/adminview/db/table_name?columns=columns](localhost:3000//adminview/db/table_name?columns=columns).
 
 ### GraphQL Voyager
-
-L'application Voyager, accessible à `/adminview/voyager`, permet de visualiser le « graphe » sous-jacent à la structure de l'API.
+L'application Voyager, accessible à [/adminview/voyager](localhost:3000/adminview/voyager), permet de visualiser le « graphe » sous-jacent à la structure de l'API.
 
 ### GraphQL Playground
-
 == Attention, comme tout GraphQL Playground est géré directement par le package apollo-server-express, les requêtes dans le Playground **ne sont pas** soumises au mêmes règles de permission que dans adminview. (D'ailleurs, `/graphql` n'est même pas un sous-path de `/adminview`.) ==
 
-Accéder via un navigateur à `/graphql` renvoie l'application GraphQL Playground.
+Accéder via un navigateur à `/graphql` renvoie vers l'application GraphQL Playground.
 
 Il s'agit du même `/graphql` que l'_endpoint_ de l'API, mais le serveur est configuré de sorte à renvoyer Playground lorsqu'il détecte un accès via navigateur. Les requêtes dans le Playground sont cependant soumises au mêmes permissions que dans l'API GraphQL [^doute]. GraphQL Playground est désactivé en production.
 
 [^doute]: euuuuh à vérifier...
 
+## Tests
+[^doute_tests]
 
-## Documentation
+Sigma possède une suite de tests unitaires, déstinés à tester les resolvers graphql.
+
+Pour executer les tests, il suffit d'utiliser la commande `npm test`.
+
+Les tests effectués sont dans test/testData.js, sous la forme d'une liste. Chaque élément contient une requête graphql, et les données qu'elle doit renvoyer. Quand les seed sont modifiées, il faudra modifier les resultats attendus également. Les tests peuvent être créés ou mis à jour en entrant la requête dans graphiql, et en copiant le resultat.
 
+[^doute_tests]: cette fonctionnalité n'a pas été utilisée ni testée depuis beaucoup trop de commits pour que cette partie soit fiable actuellement (2018-12-04).
+
+## Documentation
 La documentation détaillée du projet est [ici](./doc/index.html). Elle a été compilée avec [JSDoc](http://usejsdoc.org/index.html) sous format hmtl selon le fichier de configuration [`configfile_doc.json`](./configfile_doc.json) à la racine du projet.
 
 Le script pour faire tourner [JSDoc](http://usejsdoc.org/index.html) et régénérer la documentation est : `npm run doc`
 
 Les fichiers compilés se situent dans [`doc`](.) avec leurs fichiers image. Par nature de l'outil JSDoc il est facile de documenter en détail des fonctions .js mais plus compliqué de documenter un fichier.
 
-A la fin de ce fichier JSDoc rajoute les commentaires placés dans chacun des fichiers et des hyperliens pour y accéder.
+A chaque execution JSDoc rajoute les commentaires placés dans chacun des fichiers dans la doc de façon structurée.
+
+La structure générale du projet peut être résumé comme suit :
+
+![alt text](assets/struct.png "Structure du projet")
\ No newline at end of file
diff --git a/assets/struct.png b/assets/struct.png
new file mode 100644
index 0000000000000000000000000000000000000000..7c9e4abaeabc48289eb6b57c61c922a24e4d0594
Binary files /dev/null and b/assets/struct.png differ
diff --git a/configfile_doc.json b/configfile_doc.json
index 3c4db54b4e44b38898249152e450588b681da74e..790ec5f9bec701e6f710eb75495886408596b51a 100644
--- a/configfile_doc.json
+++ b/configfile_doc.json
@@ -1,12 +1,12 @@
 {
-    "comment": "Ce fichier sert à configurer JSDoc et permet la génération automatique de documentation facilement",
+    "comment": "Ce fichier sert à configurer JSDoc et permet la génération automatique de documentation facilement 129.104.201.90:389",
     "plugins": ["plugins/markdown", "plugins/summarize", "node_modules/jsdoc-babel"],
     "recurseDepth": 5,
     "source": {
-        "include": ["./src","./db","./README.md"],
-        "exclude": ["./db/migrations","./db/seeds"],
-        "includePattern": ".+\\.(js|ts)(doc|x)?$",
-        "excludePattern": "(^|\\/|\\\\)_"
+        "include": ["./README.md","./CONTRIBUTING.md","./src","./test","./db"],
+        "exclude": [],
+        "includePattern": "(.+\\.(js|ts)(doc|x)?$)|(.md)",
+        "excludePattern": "((^|\\/|\\\\)_)|(20|0)"
     },
     "sourceType": "module",
     "opts": {
@@ -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/memo knexjs.md b/db/memo_knexjs.md
similarity index 100%
rename from memo knexjs.md
rename to db/memo_knexjs.md
diff --git a/memo postgresql.md b/db/memo_postgresql.md
similarity index 99%
rename from memo postgresql.md
rename to db/memo_postgresql.md
index 4c0863a7622797e0f37d692158bdb4db556850f5..40092d67e621acfedbe938aabcdd979e0cc19992 100644
--- a/memo postgresql.md	
+++ b/db/memo_postgresql.md
@@ -202,4 +202,4 @@ Certains outils permettent de le faire de façon intelligente : ils permettent
 Par exemple, c'est ce que permet de faire Knex.js, utilisé par le projet sigma notamment.
 Mais ces deux notions sont plus générales. Par exemple, Symfony (le framework PHP) sait construire des fichiers migrations (i.e. choisir le schéma de votre bdd) automatiquement, en lisant les paramètres spécifiés dans les annotations de vos Entity.php. Django fait aussi un truc similaire.
 
-Un memo Knex.js est disponible... euh... quelque part, il devrait s'appeler "memo knexjs".
+Un memo Knex.js est disponible... euh... quelque part, il devrait s'appeler "memo knexjs".
\ 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 981beb87fc820a5b36dbfbd527b9fb7eb802947b..287d1de097fc798bae729c21d4a8f2db4ffdfa62 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3447,6 +3447,12 @@
       "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
       "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
     },
+    "assertion-error": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+      "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+      "dev": true
+    },
     "assign-symbols": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -3825,6 +3831,12 @@
       "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=",
       "dev": true
     },
+    "browser-stdout": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+      "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+      "dev": true
+    },
     "browserify-aes": {
       "version": "1.2.0",
       "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
@@ -4116,6 +4128,20 @@
         "lazy-cache": "^1.0.3"
       }
     },
+    "chai": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+      "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+      "dev": true,
+      "requires": {
+        "assertion-error": "^1.1.0",
+        "check-error": "^1.0.2",
+        "deep-eql": "^3.0.1",
+        "get-func-name": "^2.0.0",
+        "pathval": "^1.1.0",
+        "type-detect": "^4.0.5"
+      }
+    },
     "chalk": {
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
@@ -4172,6 +4198,12 @@
       "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=",
       "dev": true
     },
+    "check-error": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
+    },
     "chokidar": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
@@ -4689,13 +4721,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": {
@@ -4789,6 +4829,15 @@
       "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
       "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
     },
+    "deep-eql": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+      "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+      "dev": true,
+      "requires": {
+        "type-detect": "^4.0.0"
+      }
+    },
     "deep-extend": {
       "version": "0.6.0",
       "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
@@ -4955,6 +5004,12 @@
         }
       }
     },
+    "diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true
+    },
     "diffie-hellman": {
       "version": "5.0.3",
       "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -6639,6 +6694,12 @@
       "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==",
       "dev": true
     },
+    "get-func-name": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+      "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+      "dev": true
+    },
     "get-stream": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
@@ -6805,12 +6866,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": {
@@ -6887,6 +6948,12 @@
         "viz.js": "2.0.0"
       }
     },
+    "growl": {
+      "version": "1.10.5",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+      "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+      "dev": true
+    },
     "handlebars": {
       "version": "4.0.12",
       "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz",
@@ -7003,6 +7070,12 @@
         "minimalistic-assert": "^1.0.1"
       }
     },
+    "he": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
+      "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=",
+      "dev": true
+    },
     "header-case": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/header-case/-/header-case-1.0.1.tgz",
@@ -8638,6 +8711,33 @@
       "integrity": "sha512-Q2PKB4ZR4UPtjLl76JfzlgSCUZhSV1AXQgAZa1qt5RiaALFjP/CDrGvFBrOz7Ck6McPcwMAxTsJvWOUjOU8XMw==",
       "dev": true
     },
+    "mocha": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz",
+      "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==",
+      "dev": true,
+      "requires": {
+        "browser-stdout": "1.3.1",
+        "commander": "2.15.1",
+        "debug": "3.1.0",
+        "diff": "3.5.0",
+        "escape-string-regexp": "1.0.5",
+        "glob": "7.1.2",
+        "growl": "1.10.5",
+        "he": "1.1.1",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "supports-color": "5.4.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.15.1",
+          "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
+          "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
+          "dev": true
+        }
+      }
+    },
     "moment": {
       "version": "2.21.0",
       "resolved": "https://registry.npmjs.org/moment/-/moment-2.21.0.tgz",
@@ -8789,9 +8889,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": {
@@ -9367,6 +9467,12 @@
         "pify": "^3.0.0"
       }
     },
+    "pathval": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+      "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+      "dev": true
+    },
     "pause": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
@@ -11543,6 +11649,12 @@
         "prelude-ls": "~1.1.2"
       }
     },
+    "type-detect": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+      "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+      "dev": true
+    },
     "type-is": {
       "version": "1.6.16",
       "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
diff --git a/package.json b/package.json
index 128f20b1bcb31dbc9cb0688f2bd1973186f20596..dc3c2d87498bf9164549ce02cd4b441d5e704404 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,8 @@
     "start": "nodemon --watch build ./build/bundle.js",
     "start_prod": "node ./build/bundle.js",
     "doc": "jsdoc --configure configfile_doc.json",
-    "lint": "eslint --ext .js --ext .ts src/ "
+    "lint": "eslint --ext .js --ext .ts src/",
+    "test": "mocha --exit"
   },
   "repository": {
     "type": "git",
@@ -67,6 +68,7 @@
     "@types/node": "^10.12.14",
     "@types/passport": "^0.4.7",
     "babel-eslint": "^8.2.6",
+    "chai": "^4.2.0",
     "eslint": "^4.19.1",
     "eslint-config-standard": "^11.0.0",
     "eslint-loader": "^2.1.1",
@@ -74,10 +76,12 @@
     "eslint-plugin-node": "^6.0.1",
     "eslint-plugin-promise": "^3.8.0",
     "eslint-plugin-standard": "^3.1.0",
+    "graphql-request": "^1.8.2",
     "jsdoc": "^3.5.5",
     "jsdoc-babel": "^0.5.0",
     "jsdoc-to-markdown": "^4.0.1",
     "nodemon": "^1.18.8",
+    "mocha": "^5.2.0",
     "ts-loader": "^5.3.1",
     "typescript": "^3.2.2",
     "webpack": "^4.27.1",
diff --git a/src/app.ts b/src/app.ts
index b00bad015525a4ce72001ec51373e54447029afe..16810e7e0dd7d884f52c9492efee6a5784e11974 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -27,7 +27,7 @@ import morgan from 'morgan';
 // packages pour pouvoir importer des fichiers de config
 import path from 'path';
 // config des paramètres de connexion au LDAP
-import { ldapConfig, credentialsLdapConfig } from './ldap/config';
+import { ldapConfig, credentialsLdapConfig } from './ldap/internal/config';
 
 // configure passport, pour l'authentification ldap et pour comment gérer les sessions (serializeUser/deserializeUser)
 import './config_passport';
diff --git a/src/graphql/connectors/authentifiers.js b/src/graphql/connectors/authentifiers.ts
similarity index 83%
rename from src/graphql/connectors/authentifiers.js
rename to src/graphql/connectors/authentifiers.ts
index 4ad92cdbee0f0f693cd974452f7eb7d533274471..b453c639f0dfb280f9e50fbb833c093350ae57fe 100644
--- a/src/graphql/connectors/authentifiers.js
+++ b/src/graphql/connectors/authentifiers.ts
@@ -7,20 +7,29 @@ import knex from '../../../db/knex_router.js';
 import * as connectors from './connectors.js';
 import * as selectors from './selectors.js';
 import * as list_selectors from './list_selectors.js';
-import {Open as LDAPOpen, User as LDAPUser}  from '../../ldap/users';
-import {Admin as LDAPAdmin, Supervisor as LDAPSupervisor} from '../../ldap/admins';
 
 /**
  * @summary Place-holder permettant de concaténer utilisateur et permissions dans un seul objet
  * @arg {Object} user - Représente l'utilisateur qui a effectué la requête.
- * @arg {Object} ldap_access - Représente les permissions de l'utilisateur en question.  
  * @return {Object} Prototype de la fonction contenant la concaténation des deux arguments.
  */
-function User(user, ldap_access){ //hawkspar->akka ; ceci pourrait avantageusement être une classe
-    this.anonymous = Boolean(user.anonymous),
-    this.uid = user.uid,
-    this.password = user.password,
-    this.ldap_access = ldap_access;
+// function OldUser(user, ldap_access){ //hawkspar->akka ; ceci pourrait avantageusement être une classe
+//     this.anonymous = Boolean(user.anonymous),
+//     this.uid = user.uid,
+//     this.password = user.password,
+//     this.ldap_access = ldap_access;
+// }
+
+/**
+ * @summary un objet typé pour représenter la personne effectuant la requête. A ne pas confondre avec l'objet représentant un utilisateur
+ */
+class QUser{
+    anonymous : boolean;
+    uid : string;
+    constructor(auth, uid){
+        this.anonymous = !auth;
+        this.uid = uid;
+    }
 }
 
 /**
@@ -31,18 +40,18 @@ function User(user, ldap_access){ //hawkspar->akka ; ceci pourrait avantageuseme
  * @rights user
  */
 export function anonymous(user){
-    return new User(user, new LDAPOpen());
+    return new QUser(true, null);
 }
 
 /**
- * @summary Authentifie un utilisateur comme viewer(groupUID) en appelant {@link User} et {@link LDAPUser}
+ * @summary Authentifie un utilisateur comme authentifié
  * @arg {Object} user - Représente l'utilisateur qui a effectué la requête.
  * * @return {Promise(Object)} Un objet user si l'utilisateur possède les droits indiqués, 
  * null sinon
  * @rights user
  */
-export function loggedIn(user, groupUID){ //hawkspar: WTF ? Pq garder son groupUID ?
-    return new User(user, new LDAPUser(user));
+export function loggedIn(user){ //hawkspar: WTF ? Pq garder son groupUID ?
+    return new QUser(false, user.uid);
 }
 
 /**
diff --git a/src/graphql/connectors/connectors.js b/src/graphql/connectors/connectors.ts
similarity index 77%
rename from src/graphql/connectors/connectors.js
rename to src/graphql/connectors/connectors.ts
index f1d017e6d57f0670401dc01fdd2c16a8ef02ec4b..2e2aaba74cd7b687581635cb7c36301f563a2676 100644
--- a/src/graphql/connectors/connectors.js
+++ b/src/graphql/connectors/connectors.ts
@@ -5,21 +5,66 @@
 import knex from '../../../db/knex_router';
 import * as selectors from './selectors';
 import * as list_selectors from './list_selectors';
-import {Open as LDAPOpen, User as LDAPUser}  from '../../ldap/users';
 
+import {Group as groupLdap, groupData} from '../../ldap/export/group';
+import {User as userLdap, userData} from '../../ldap/export/user';
+import { isExportNamedDeclaration } from 'babel-types';
+import { admin } from './authentifiers';
+
+class Group{
+    gid : string;
+    type : string; // simple or meta - not to be confused with category
+
+    name? : string;
+    category? : string;
+    members? : string[];
+    admins? : string[];
+    description? : string;
+    website?: string;
+    createdAt?: string;
+    UpdatedAt?: string;
+
+    constructor(obj){
+        this.gid = obj.uid; // until the database is mutated, the gid is called uid in the database
+        this.type = obj.type;
+        this.description = obj.description;
+        this.website = obj.website;
+        this.createdAt = obj.createdAt;
+        this.UpdatedAt = obj.updatedAt;
+    }
 
-const utilisateur = new LDAPOpen();
+    /**
+     * @summary effectue une requête au ldap pour avoir les donnees du groupe.
+     */
+    async fetchData() : Promise<void>{
+        const data : groupData = await groupLdap.peek(this.gid);
+        this.name = data.name;
+        this.category = data.type; // this fields needs to be renamed in groupData
+        this.members = data.members;
+        this.admins = data.admins;
+        this.description = data.description || this.description;
+    }
+}
 
-let result = utilisateur.getMembers("br").then(res => {
-    console.log("Got it");
-    return res;
-});
+class User{
+    uid : string;
 
-export { utilisateur };
-/* Ce n'est pas comme ça qu'on est censé fonctionner. Toute utilisation de utilisateur
- a vocation à être temporaire, et sera remplacé par l'usage des fonctions 
- d'authentification correctes
-*/
+    constructor(obj){
+        this.uid = obj.uid;
+    }
+
+    async fetchData() : Promise<void>{
+        const data : userData = await userLdap.peek(this.uid);
+        for(const field in data){
+            this[field] = data[field];
+        }
+    }
+}
+
+class QUser{
+    uid: string;
+    password : string;
+}
 
 /*
     Le tag @rights est la gestion des autorisations.
@@ -100,7 +145,7 @@ function getGroupTableName(wantedType){
     }
 }
 
-export function rasifyGroupUID(uid){ //hawkspar->akka ; je plussoie le nom mais pas très lisible
+export function rasifyGID(uid){ //hawkspar->akka ; je plussoie le nom mais pas très lisible
     return String(uid).replace(' ', '_').replace(/\W/g, '').toLowerCase();
 }
 
@@ -109,19 +154,19 @@ export function rasifyGroupUID(uid){ //hawkspar->akka ; je plussoie le nom mais
  * @desc Pour l'instant, la fonction effectue la même requête que `getAllVisibleGroups` 
  * et restreint au groupe demandé. Cette fonction peut être implémentée de manière 
  * plus efficace et plus chiante.
- * @arg {Object} user - Utilisateur effectuant la requête. 
- * @arg {String} uid - Identifiant du groupe voulu.
+ * @arg {String} gid - Identifiant du groupe voulu.
  * @arg {String} type - Type de groupe voulu. `"simple"`, `"meta"` ou `"all"`.
  * @return {Promise(group)} Retour de requête knex. Le groupe demandé, si l'utilisateur a le droit de la voire.
  * @rights user
  */
-export async function getGroupIfVisible(user, groupUID, type="all"){
+export async function getGroupIfVisible(quser : QUser, gid, type="all") : Promise<Group>{
     let group_table_name = getGroupTableName(type);
-    let visible_groups = await selectors.visibleGroups(user);
-    return knex.with('visible_groups', visible_groups).select()
+    let visible_groups = await selectors.visibleGroups(quser);
+    const res = await knex.with('visible_groups', visible_groups).select()
         .from(group_table_name).innerJoin('visible_groups', function (){
             this.on('visible_groups.uid', '=', group_table_name + '.uid');
-        }).where(group_table_name + '.uid', groupUID).then(res => res[0]);
+        }).where(group_table_name + '.uid', gid);
+    return res[0];
 }
 
 export const getSimpleGroupIfVisible = (user, groupUID) => getGroupIfVisible(user, groupUID, "simple");
@@ -135,9 +180,9 @@ export const getMetaGroupIfVisible = (user, groupUID) => getGroupIfVisible(user,
  * @return {Promise} Retour de requête knex. Liste de tous les groupes que l'utilisateur a le droit de voire.
  * @rights user
  */
-export async function getAllVisibleSimpleGroups (user){
-    let visible_groups = await selectors.visibleGroups(user);
-    return getSimpleGroupsFromCallbacks(user, visible_groups);
+export async function getAllVisibleSimpleGroups(quser : QUser) : Promise<Group[]>{
+    let visible_groups = await selectors.visibleGroups(quser);
+    return getSimpleGroupsFromCallbacks(quser, visible_groups);
 }
 
 /**
@@ -148,54 +193,72 @@ export async function getAllVisibleSimpleGroups (user){
  * @return {Promise} Retour de requête knex. Liste de tous les groupes que l'utilisateur a le droit de voire.
  * @rights user
  */
-export async function getAllVisibleMetaGroups (user){
-    let visible_groups = await selectors.visibleGroups(user);
-    return getMetaGroupsFromCallbacks(user, visible_groups);
+export async function getAllVisibleMetaGroups(quser : QUser) : Promise<Group[]>{
+    let visible_groups = await selectors.visibleGroups(quser);
+    return getMetaGroupsFromCallbacks(quser, visible_groups);
 }
 
 /**
  * @summary Renvoie tous les groupes visibles par l'utilisateur user
  * @desc Cette fonction effectue une requête knex. Elle gère l'arête de parenté.
- * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. 
  * @arg {String} wantedType - Type de groupe voulu : `"simple"`, `"meta"` ou `"all"`. 
  * @return {Promise} Retour de requête knex. Liste de tous les groupes que l'utilisateur a le droit de voire.
  * @rights user
  */
-export async function getAllVisibleGroups(user){
-    let visible_groups = await selectors.visibleGroups(user);
-    return getGroupsFromCallbacks(user, visible_groups);
+export async function getAllVisibleGroups(quser : QUser) : Promise<Group[]>{
+    let visible_groups = await selectors.visibleGroups(quser);
+    return getGroupsFromCallbacks(quser, visible_groups);
 }
 
 /**
  * @summary Teste si un utilisateur est membre d'un groupe
- * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. 
- * @arg {Object} groupUID - L'id du groupe dont on veu savoir si l'utilisateur est membre. 
  * @return {Promise(Boolean)} Boolean indiquant si l'utilisateur est membre du groupe.
  * @rights user
  */
-export async function isMember(user, groupUID){
-    let member_list = await getGroupMemberUsers(user, groupUID);
-    return member_list && (member_list.indexOf(groupUID) != -1);
+export async function isMember(quser : QUser, group : Group) : Promise<boolean>{
+    let member_set = await getGroupMemberUsersSet(quser, group);
+    if( member_set && member_set.has(quser.uid) ){
+        return true;
+    }else{
+        return isSpeaker(quser, group);
+    }
+}
+
+export async function isSpeaker(quser : QUser, group : Group) : Promise<boolean>{
+    return isAdmin(quser, group);
+}
+
+export async function isAdmin(quser : QUser, group : Group) : Promise<boolean>{
+    let admin_set = await getGroupAdminUsersSet(quser, group);
+    if( admin_set && admin_set.has(quser.uid) ){
+        return true;
+    }else{
+        return isSupervisor(quser, group);
+    }
+}
+
+export async function isSupervisor(quser : QUser, group : Group) : Promise<boolean>{
+    // TODO : implement
+    return true;
 }
 
 /**
- * @summary Attribue un UID qui n'a pas encore été utilisé à un groupe
+ * @summary Attribue un GID qui n'a pas encore été utilisé à un groupe
  * @desc RASifie le string initialUID si necessaire (ramené à de l'ASCCI sans espace), puis si l'uid est deja pris rajoute un n a la fin et reteste
  * @arg {String} uid - L'uid du groupe dont on veut les administrateurs. 
  * @return {Promise} Retour de requête knex. Promise qui renvera une liste de tous les utilisateurs ayant droit d'admin sur le groupe
  * @rights user
  * remarque : n'importe qui peut tester si un groupe existe en demandant a créer un groupe avec ce nom la et en regardant si
- * son UID a été modifié. Je ne vois pas comment contourner ce problème, c'est donc une faille permanente de sigma.
+ * son GID a été modifié. Je ne vois pas comment contourner ce problème, c'est donc une faille permanente (mineure) de sigma.
  */
-export function getAvailablegroupUID(initialUID){
-    let rasUID = rasifyGroupUID(initialUID);
-    return knex.from('groups').where('uid', rasUID).then(res => {
-        if (res.length == 0) {
-            return (rasUID);
-        } else {
-            return (getAvailablegroupUID(rasUID + 'n'));
-        }
-    });
+export async function getAvailableGID(initialGID : string) : Promise<string>{
+    let rasGID = rasifyGID(initialGID);
+    const res = knex.from('groups').where('uid', rasGID)
+    if (res.length == 0) {
+        return (rasGID);
+    } else {
+        return (getAvailableGID(rasGID + 'n'));
+    }
 }
 
 /**
@@ -210,13 +273,16 @@ export function getAvailablegroupUID(initialUID){
  * @return {Promise} Retour de requête knex. Le groupe qui vient d'être créé. En cas d'echec, renvoie une erreur.
  * @rights admin (args.parent_uid)
  */
-export async function createSubgroup(user, args){
+export async function createSubgroup(quser : QUser, group : Group, args) : Promise<Group>{
+
+    // TODO : finish
+
     if (typeof args.parent_uid != 'string')
         throw "Illegal argument : parent_uid must be a non null string";
     if (typeof args.name != 'string')
         throw "Illegal argument : name must be a non null string";
 
-    let rasUID = await getAvailablegroupUID(args.uid);
+    let rasUID = await getAvailableGID(args.uid);
 
     // TODO : appeller une fonction de LDAPUser pour y créer un groupe.
     await knex('simple_groups').insert({
@@ -231,7 +297,7 @@ export async function createSubgroup(user, args){
         type : "simple"
     });
 
-    return getGroupIfVisible(user, rasUID);
+    return getGroupIfVisible(quser, rasUID);
 }
 
 /**
@@ -244,9 +310,9 @@ export async function createSubgroup(user, args){
  * @return {Promise} Retour de requête knex. Le groupe qui vient d'être créé. En cas d'echec, renvoie une erreur.
  * @rights user
  */
-export async function createGroupIfLegal(user, args){
-    if( await LDAPOpen.isGroupAdmin(utilisateur, args.parentuid) ){
-        return createSubgroup(user, args);
+export async function createGroupIfLegal(quser : QUser, group : Group, args) : Promise<Group>{
+    if( await isAdmin(quser, group) ){
+        return createSubgroup(quser, group, args);
     }else{
         throw "illegal request : you must have admin rights over a group to create a subgroup of that group";
     }
@@ -261,15 +327,16 @@ export async function createGroupIfLegal(user, args){
  * @return {Promise(Object)} Retour de requête knex. Toutes les requêtes destinées au groupe.
  * @rights admin(recipientUID)
  */
-export async function getUserJoinGroupRequests(user, recipientUID){
+export async function getUserJoinGroupRequests(quser : QUser, recipient : Group) : Promise<Object>{
     let result = knex.select('id', 'useruid', 'message').from('user_join_group')
-        .where('recipient', recipientUID);
+        .where('recipient', recipient.gid);
     return result.map( obj => {
         obj.type = "UserJoinGroup";
         return obj;
     });
 }
 
+
 /**
  * @summary Renvoie toues les requêtes de type GroupJoinEvent 
  * @desc Une requête UserJoinGroup est envoyée par un groupe à un évènement (donc aux administrateurs de l'évènement), 
@@ -281,9 +348,9 @@ export async function getUserJoinGroupRequests(user, recipientUID){
  * @return {Promise(Object)} Retour de requête knex. Toutes les requêtes destinées au groupe.
  * @rights speaker(recipientUID)
  */
-export async function getGroupJoinEventRequests(user, recipientUID){
+export async function getGroupJoinEventRequests(quser : QUser, recipient : Group) : Promise<Object>{
     let result = await knex.select('id', 'senderuid', 'eventuid', 'message').from('group_join_event')
-        .where('recipient', recipientUID);
+        .where('recipient', recipient.gid);
     return result.map( obj => {
         obj.type = "GroupJoinEvent";
         return obj;
@@ -292,7 +359,7 @@ export async function getGroupJoinEventRequests(user, recipientUID){
 
 
 /**
- * @summary Renvoie toues les requêtes de type GroupJoinEvent 
+ * @summary Renvoie toues les requêtes de type GroupHostEvent 
  * @desc Une requête UserJoinGroup est envoyée par un groupe à un évènement (donc aux administrateurs de l'évènement), 
  * pour demander à rejoindre cet évènement.
  * Remarque : toutes les requêtes ont pour le moment un attribut recipient, 
@@ -302,9 +369,9 @@ export async function getGroupJoinEventRequests(user, recipientUID){
  * @return {Promise(Object)} Retour de requête knex. Toutes les requêtes destinées au groupe.
  * @rights speaker(recipientUID)
  */
-export async function getYourGroupHostEventRequests(user, recipientUID){
+export async function getYourGroupHostEventRequests(quser : QUser, recipient : Group) : Promise<Object>{
     let result = await knex.select('id', 'senderuid', 'eventuid', 'message').from('your_group_host_event')
-        .where('recipient', recipientUID);
+        .where('recipient', recipient.gid);
     return result.map( obj => {
         obj.type = "YourGroupHostEvent";
         return obj;
@@ -312,33 +379,6 @@ export async function getYourGroupHostEventRequests(user, recipientUID){
 }
 
 
-//Don't forget the argument user is the guy who makes the request, not the user we want
-export const getUser = (user, uid, db) => { 
-    const refactorer = (data) => {
-        if (typeof data.brRoom == 'string') data.brRoom = [data.brRoom];
-
-        return {
-            uid: uid,
-            lastName: data.sn,
-            givenName: data.givenName,
-            nickname: data.displayName,
-            nationality: data.country,
-            birthdate: data.brBirthdate,
-            groups: data.brMemberOf,
-            mail: data.mail,
-            phone: data.telephoneNumber,
-            address: data.brRoom,
-            promotion: data.brPromo
-        };
-    };
-
-    const result = utilisateur.getUser(uid).then(res => {
-        return refactorer(res[0]);
-    });
-
-    return result;
-};
-
 // All these messages are returned if they are visible
 
 export async function getAnnouncement(user, messageID){
@@ -403,7 +443,7 @@ export async function getAnswer(user, messageID){
  * @param {*} eventID - Identifiant unique de l'événement.
  * @rights super
  */
-export async function getMessage(user, messageID){
+export async function getMessage(user, messageID) : Promise<any>{
     return getEvent(user, messageID) | 
         getAnnouncement(user, messageID) |
         getPrivatePost(user, messageID) |
@@ -606,42 +646,70 @@ export function releaseAdminRights(user, groupUID){
  * @author akka vodol 
  * @rights member(metaGroupUID)
  */
-export async function getGroupMemberUsers(user, GroupUID){
-    let type = await list_selectors.getGroupType(user, GroupUID);
-    switch( type ){
-    case "SimpleGroup":
-        return utilisateur.getMembers(GroupUID);
-        // return ["anatole.romon"];
-    case "MetaGroup":
-        return getMetaGroupMemberUsers(user, GroupUID);
-    default:
-        return undefined;
-    }
+export async function getGroupMemberUsers(quser : QUser, group : Group) : Promise<User[]>{
+    const memberSet = await getGroupMemberUsersSet(quser, group);
+    return Array.from(memberSet.values()).map( uid => new User(uid) );
+}
+
+async function getGroupMemberUsersSet(quser : QUser, group : Group) : Promise<Set<string>>{
+    switch( group.type ){
+        case "simple":
+            if(!group.members){
+                await group.fetchData();
+            }
+            return new Set(group.members);
+        case "meta":
+            let member_group_list = await selectors.metaGroupMembers(quser, group).then(cb => cb(knex));
+            let members = new Set;
+            for(const memberGroup of member_group_list){
+                let res = await getGroupMemberUsers(quser, new Group(memberGroup));
+                for(const member of res.values()){
+                    members.add(member);
+                }
+            }
+            return members;
+        default:
+            return undefined;
+        }
 }
+
+
 /**
- * @summary Renvoie les membres d'un meta groupe.
+ * @summary Renvoie les membres d'un groupe quelquonque.
  * @param {Object} user - Utilisateur effectuant la requête.
  * @param {String} metaGroupUID - Identifiant unique du groupe.
  * @return {Promise(List)} Une liste des uid de tous les membres du groupe
  * @author akka vodol 
  * @rights member(metaGroupUID)
  */
-export async function getMetaGroupMemberUsers(user, metaGroupUID){
-    let member_group_list = await selectors.metaGroupMembers(user, metaGroupUID).then(cb => cb(knex));
-    let members = [];
-    for(let memberGroup of await member_group_list){
-        members = members.concat(getGroupMemberUsers(user, metaGroupUID));
-    }
+export async function getGroupAdminUsers(quser : QUser, group : Group) : Promise<User[]>{
+    const memberSet = await getGroupAdminUsersSet(quser, group);
+    return Array.from(memberSet.values()).map( uid => new User(uid) );
+}
+
+async function getGroupAdminUsersSet(quser : QUser, group : Group) : Promise<Set<string>>{
+    switch( group.type ){
+        case "simple":
+            if(!group.members){
+                await group.fetchData();
+            }
+            return new Set(group.admins);
+        case "meta":
+            // TODO : Meta group administration not yet implemented
+            return new Set([]);
+        default:
+            return undefined;
+        }
 }
 
-export async function getSimpleGroupsFromCallbacks (user, selection){
+async function getSimpleGroupsFromCallbacks (user, selection){
     return knex.with('selection', selection).select("simple_groups.*").from("simple_groups")
         .innerJoin('selection', function (){
             this.on('selection.uid', '=', 'simple_groups.uid');
         });
 }
 
-export async function getMetaGroupsFromCallbacks (user, selection){
+async function getMetaGroupsFromCallbacks (user, selection){
     return knex.with('selection', selection).select().from("meta_groups")
         .innerJoin('selection', function (){
             this.on('selection.uid', '=', 'meta_groups.uid');
@@ -656,7 +724,7 @@ export async function getMetaGroupsFromCallbacks (user, selection){
  * @return {Promise} Retour de requête knex. Liste de tous les groupes que l'utilisateur a le droit de voire.
  * @rights user
  */
-export async function getGroupsFromCallbacks(user, cbList){
+async function getGroupsFromCallbacks(user, cbList){
     // console.log(cbList);
     let all_simple_groups = await getSimpleGroupsFromCallbacks(user, cbList);
     let all_meta_groups = await getMetaGroupsFromCallbacks(user, cbList);
diff --git a/src/graphql/new_connectors/authorisation.ts b/src/graphql/new_connectors/authorisation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c8b47227275ea5f0134f7c0565e59b407fc0480a
--- /dev/null
+++ b/src/graphql/new_connectors/authorisation.ts
@@ -0,0 +1,65 @@
+import { QueryBuilder } from "knex";
+
+
+interface QUser {
+    uid : string
+};
+
+export type GroupSet = Set<string>;
+
+
+/**
+ * There are 6 levels of authorisation for a group
+ *  none : can't even know the group exists
+ *  viewer : can see the group
+ *  memebr : part of the group
+ *  speaker : allowed to speak for the group
+ *  admin : admin of the group
+ *  supervisor : allowed to take control of the group
+ * 
+ * Levels 0 and 5 are a bit special. For levels 1 - 4, the functions are as follows
+ */
+
+// These functions return the list of all groups that the query user (quser) has corresponding priviledge for
+
+export async function forViewer(quser : QUser) : Promise<GroupSet>{
+    throw new Error("Not implemented");
+}
+
+export async function forMember(quser : QUser) : Promise<GroupSet>{
+    throw new Error("Not implemented");
+}
+
+export async function forSpeaker(quser : QUser) : Promise<GroupSet>{
+    throw new Error("Not implemented");
+}
+
+export async function forAdmin(quser : QUser) : Promise<GroupSet>{
+    throw new Error("Not implemented");
+}
+
+// These functions test if the quser has corresponding priviledges for the given group
+
+export async function isViewer(quser : QUser, gid : string) : Promise<boolean>{
+    let groupSet = await forViewer(quser);
+    return groupSet.has(gid);
+}
+
+export async function isMember(quser : QUser, gid : string) : Promise<boolean>{
+    let groupSet = await forMember(quser);
+    return groupSet.has(gid);
+}
+
+export async function isSpeaker(quser : QUser, gid : string) : Promise<boolean>{
+    let groupSet = await forSpeaker(quser);
+    return groupSet.has(gid);
+}
+
+export async function isAdmin(quser : QUser, gid : string) : Promise<boolean>{
+    let groupSet = await forAdmin(quser);
+    return groupSet.has(gid);
+}
+
+export async function isSupervisor(quser : QUser, gid : string) : Promise<boolean>{
+    throw new Error('Not implemented');
+};
\ No newline at end of file
diff --git a/src/graphql/new_connectors/connection.ts b/src/graphql/new_connectors/connection.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ea207de1b2c27c68d7396de48c86508b72f015f6
--- /dev/null
+++ b/src/graphql/new_connectors/connection.ts
@@ -0,0 +1,612 @@
+/**
+ * @file Fonctions pour interagir avec la BDD sigma et le LDAP.
+ * @author akka vodol
+ */
+import knex from '../../../db/knex_router';
+
+import {Group as groupLdap, groupData} from '../../ldap/export/group';
+import {User as userLdap, userData} from '../../ldap/export/user';
+import { isExportNamedDeclaration } from 'babel-types';
+
+import {GroupSet} from './authorisation';
+
+export class Group{
+    gid : string;
+    type : string; // simple or meta - not to be confused with category
+    name? : string;
+    category? : string;
+    members? : string[];
+    admins? : string[];
+    description? : string;
+    website?: string;
+    createdAt?: string;
+    UpdatedAt?: string;
+
+    constructor(obj){
+        this.gid = obj.uid; // until the database is mutated, the gid is called uid in the database
+        this.type = obj.type;
+        this.description = obj.description;
+        this.website = obj.website;
+        this.createdAt = obj.createdAt;
+        this.UpdatedAt = obj.updatedAt;
+    }
+
+    /**
+     * @summary effectue une requête au ldap pour avoir les donnees du groupe.
+     */
+    async fetchData() : Promise<void>{
+        const data : groupData = await groupLdap.peek(this.gid);
+        this.name = data.name;
+        this.category = data.type; // this fields needs to be renamed in groupData
+        this.members = data.members;
+        this.admins = data.admins;
+        this.description = data.description || this.description;
+    }
+}
+
+export class User{
+    uid : string;
+
+    constructor(obj){
+        this.uid = obj.uid;
+    }
+
+    async fetchData() : Promise<void>{
+        const data : userData = await userLdap.peek(this.uid);
+        for(const field in data){
+            this[field] = data[field];
+        }
+    }
+}
+
+class QUser{
+    uid: string;
+    password : string;
+}
+
+/*
+    Le tag @rights est la gestion des autorisations.
+
+    Le système GraphQL est pensé comme l'interface par laquelle les utilisateurs 
+    intéragissent avec sigma, les graphismes en moins.
+    Le client peut envoyer tout type de requête. C'est au niveau des resolvers
+    que les permissions sont gérées. D'où le @rights
+
+    Commençons par un rappel sur le fonctionnement des droits. 
+    Chaque utilisateur a un certain niveau de droit sur chaque groupe. Ce niveau de droit indique
+    ce qu'il a le droit de savoir et de faire. Chaque niveau est inclus dans les niveaus supérieur.
+    Les différents niveaux sont :
+    none - aucun droit
+    viewer : l'utilisateur a visibilité sur le groupe. Il sait que le groupe existe, et a accès à un certain nombre d'infos.
+    member : l'utilisateur est membre du groupe
+    speaker : l'utilisateur peut parler au nom du groupe. Il a le droit de publier des annonces et d'organiser des évènements
+    admin : l'utilisateur a tous les droits sur le groupe
+
+    Certaines fonctions de connectors effectuent des vérifications d'authorisations avant 
+    de renvoyer une réponse, d'autres non. Pour être sur qu'on ne renvoie jamais de réponse
+    sans avoir au préalable éffectué les bonnes vérifications, chaque fonction possède dans sa
+    description un attribut droit, qui décrit les droits que fournit cette fonction.
+
+    La valeur de @rights peut être :
+    super - la fonction n'effectue aucune véri-fication, et renvoie le resultat demandé
+    admin( groupUID ) - la fonction ne fait que ce qu'un admin du groupe indiqué aurait le droit de faire
+    speaker( groupUID ), member( groupUID ), veiwer( groupUID ) - même chose
+    user - la fonction ne fait que ce que l'utiliateur a le droit de faire (vérifications via l'argument user)
+
+    La procédure a suivre est la suivante : quand une fonction possède un certain niveau de droit, 
+    elle ne peut appeler une fonction possédant un niveau de droit plus large que si 
+    1 ) on a au préalable vérifié que l'utilisateur possédait effectivement ces droits. 
+    ou
+    2 ) on s'est assuré que l'opération effectuée par cet appel particulier de la fonction était dans les droits
+    de l'utilisateur
+
+    Les resolvers de base de mutation et query ont des droits user.
+
+    Les fonctions qui ne modifient pas la BDD et ne renvoient pas de données sur la BDD n'ont pas de rights.
+*/
+
+export function rasifyGID(uid){ //hawkspar->akka ; je plussoie le nom mais pas très lisible
+    return String(uid).replace(' ', '_').replace(/\W/g, '').toLowerCase();
+}
+
+/**
+ * @summary Renvoie tous les groupes simples dont les id sont dans groupSet
+ */
+export async function getSimpleGroups(groupSet : GroupSet) : Promise<Group[]>{
+    const res = await knex.select().from('simple_groups').whereIn('uid', groupSet.entries());
+    return res.map( (data) => new Group(data) );
+}
+
+/**
+ * @summary Renvoie tous les groupes simples dont les id sont dans groupSet
+ */
+export async function getMetaGroups(groupSet : GroupSet) : Promise<Group[]>{
+    const res = await knex.select().from('meta_groups').whereIn('uid', groupSet.entries());
+    return res.map( (data) => new Group(data) );
+}
+
+/**
+ * @summary Renvoie tous les groupes simples dont les id sont dans groupSet
+ */
+export async function getGroups(groupSet : GroupSet) : Promise<Group[]>{
+    const res = await knex.select().from('groups').whereIn('uid', groupSet.entries());
+    return res.map( (data) => new Group(data) );
+}
+
+/**
+ * @summary Attribue un GID qui n'a pas encore été utilisé à un groupe
+ * @desc RASifie le string initialUID si necessaire (ramené à de l'ASCCI sans espace), puis si l'uid est deja pris rajoute un n a la fin et reteste
+ * @arg {String} uid - L'uid du groupe dont on veut les administrateurs. 
+ * @return {Promise} Retour de requête knex. Promise qui renvera une liste de tous les utilisateurs ayant droit d'admin sur le groupe
+ * @rights user
+ * remarque : n'importe qui peut tester si un groupe existe en demandant a créer un groupe avec ce nom la et en regardant si
+ * son GID a été modifié. Je ne vois pas comment contourner ce problème, c'est donc une faille permanente (mineure) de sigma.
+ */
+export async function getAvailableGID(initialGID : string) : Promise<string>{
+    let rasGID = rasifyGID(initialGID);
+    const res = knex.from('groups').where('uid', rasGID)
+    if (res.length == 0) {
+        return (rasGID);
+    } else {
+        return (getAvailableGID(rasGID + 'n'));
+    }
+}
+
+/**
+ * @summary Créé un groupe si les arguments sont tous valides
+ * @desc Les arguments doivent être valides, sauf pour uid. Une clé uid valide sera générée dans tous les cas. 
+ * Les authorisations de l'utilisateur ne sont pas vérifiées
+ * On teste si l'utilisateur qui envoie la requête a des droits d'admin sur le parent du groupe qui doit être créé, avec la fonction
+ * `getUsersWithAdminRights`.
+ * Si un argument est invalide ou si l'utilisateur n'a pas les droits, la fonction renvoie une erreur
+ * @arg {Object} user - L'utilisateur qui effectue la requête. 
+ * @arg {Object} args - Les arguments envoyés à la mutation. Cf le schéma GraphQL 
+ * @return {Promise} Retour de requête knex. Le groupe qui vient d'être créé. En cas d'echec, renvoie une erreur.
+ * @rights admin (args.parent_uid)
+ */
+export async function createSubgroup(quser : QUser, group : Group, args) : Promise<Group>{
+
+    // TODO : finish
+
+    if (typeof args.parent_uid != 'string')
+        throw "Illegal argument : parent_uid must be a non null string";
+    if (typeof args.name != 'string')
+        throw "Illegal argument : name must be a non null string";
+
+    let rasGID = await getAvailableGID(args.uid);
+
+    // TODO : appeller une fonction de LDAPUser pour y créer un groupe.
+    await knex('simple_groups').insert({
+        uid: rasGID,
+        parent_uid: args.parent_uid,
+        createdAt: knex.fn.now(),
+        updatedAt: this.createdAt,
+        name: args.name,
+        website: args.website,
+        description: args.description,
+        school: args.school,
+        type : "simple"
+    });
+
+    return getGroup(rasGID);
+}
+
+export async function getUser(uid : string){
+    return new User(uid);
+}
+
+export async function searchUsers(args){
+    const searchData = {
+        givenName: args.givenName,
+        lastName: args.lastName,
+        nickname: args.nickname,
+        nationality: args.nationality,
+        school: args.school,
+        promotion: args.promotion,
+        groups: args.groups,
+        studies: args.studies,
+        sport: args.sport,
+        phone: args.phone,
+        mail: args.mail,
+        adress: args.adress,
+        ip: args.ip
+    }
+    const userList = await userLdap.search(searchData);
+    return userList.map( (uid) => new User(uid) );
+}
+
+/**
+ * @summary Renvoie toues les requêtes de type UserJoinGroup 
+ * @desc Une requête UserJoinGroup est envoyée par un utilisateur à un groupe, 
+ * pour demander à rejoindre ce groupe
+ */
+export async function getUserJoinGroupRequests(recipient : Group){
+    let result = knex.select('id', 'useruid', 'message').from('user_join_group')
+        .where('recipient', recipient.gid);
+    return result.map( obj => {
+        obj.type = "UserJoinGroup";
+        return obj;
+    });
+}
+
+/**
+ * @summary Renvoie toues les requêtes de type GroupJoinEvent 
+ * @desc Une requête UserJoinGroup est envoyée par un groupe à un évènement (donc aux administrateurs de l'évènement), 
+ * pour demander à rejoindre cet évènement.
+ * Remarque : toutes les requêtes ont pour le moment un attribut recipient, 
+ * mais ici il ne sera a terme pas utilisé.
+ */
+export async function getGroupJoinEventRequests(quser : QUser, recipient : Group){
+    let result = await knex.select('id', 'senderuid', 'eventuid', 'message').from('group_join_event')
+        .where('recipient', recipient.gid);
+    return result.map( obj => {
+        obj.type = "GroupJoinEvent";
+        return obj;
+    });
+}
+
+/**
+ * @summary Renvoie toues les requêtes de type GroupHostEvent 
+ * @desc Une requête UserJoinGroup est envoyée par un groupe à un évènement (donc aux administrateurs de l'évènement), 
+ * pour demander à rejoindre cet évènement.
+ * Remarque : toutes les requêtes ont pour le moment un attribut recipient, 
+ * mais ici il ne sera a terme pas utilisé.
+ */
+export async function getYourGroupHostEventRequests(quser : QUser, recipient : Group){
+    let result = await knex.select('id', 'senderuid', 'eventuid', 'message').from('your_group_host_event')
+        .where('recipient', recipient.gid);
+    return result.map( obj => {
+        obj.type = "YourGroupHostEvent";
+        return obj;
+    });
+}
+
+
+// All these messages are returned if they are visible
+
+// TODO : figure out which announcements are visible
+
+export async function getAnnouncement(messageID){
+    let res = await knex.select().from('announcements').where('id', messageID);
+    if(res[0]){
+        res[0].type = 'Announcement';
+        return res[0];
+    }
+    res = await knex.select().from('events').where('id', messageID);
+    if(res[0]){
+        res[0].type = 'Announcement';
+        return res[0];
+    }
+    return undefined;
+}
+
+export async function getEvent(messageID){
+    let res = await knex.select().from('events').where('id', messageID);
+    if(res[0]){
+        res[0].type = 'Event';
+        return res[0];
+    }
+    return undefined;
+}
+
+export async function getPrivatePost(messageID){
+    let res = await knex.select().from('private_posts').where('id', messageID);
+    if(res[0]){
+        res[0].type = 'PrivatePost';
+        return res[0];
+    }
+    return undefined;
+}
+
+export async function getQuestion(messageID){
+    let res = await knex.select().from('questions').where('id', messageID);
+    if(res[0]){
+        res[0].type = 'Question';
+        return res[0];
+    }
+    return undefined;
+}
+
+export async function getAnswer(messageID){
+    let res = await knex.select().from('answers').where('id', messageID);
+    if(res[0]){
+        res[0].type = 'Answer';
+        return res[0];
+    }
+    return undefined;
+}
+
+/**
+ * 
+ * @param groupSet The set of all groups who's events we want to see.
+ */
+export async function allEvents(groupSet : GroupSet){
+    let selection = [];
+    throw new Error('Not implemented');
+    let result = await knex.select().from('events').whereIn('id', selection);
+    for(let r of result){
+        r.type = 'Announcement';
+    }
+    return result;
+}
+
+export async function allAnnouncements(groupSet : GroupSet){
+    let selection = [];
+    throw new Error('Not implemented');
+    let result = await knex.select().from('announcements').whereIn('id', selection);
+    result = result.concat(
+        await knex.select().from('events').whereIn('id', selection)
+    );
+    for(let r of result){
+        r.type = 'Announcement';
+    }
+    return result;
+}
+
+export async function receivedPrivatePosts(group : Group){
+    let selection = [];
+    throw new Error('Not implemented');
+    // let result = await knex('private_posts').select().whereIn('id', received_messages);
+    // for(let entry of result){
+    //     entry.type = "PrivatePost";
+    // }
+    // return result;
+}
+
+export async function receivedQuestions(group : Group){
+    let selection = [];
+    throw new Error('Not implemented');
+    // let result = await knex('questions').select().whereIn('id', received_messages);
+    // for(let entry of result){
+    //     entry.type = "Question";
+    // }
+    // return result;
+}
+
+export async function receivedAnswers(group : Group){
+    let selection = [];
+    throw new Error('Not implemented');
+    // let received_messages = await selectors.recievedMessages(user, groupUID);
+    // let result = await knex('answers').select().whereIn('id', received_messages);
+    // for(let entry of result){
+    //     entry.type = "Answer";
+    // }
+    // return result;
+}
+
+
+export async function visibleMessages(user, messageID){
+
+}
+
+export async function getMessageGroupAuthors(user, messageID){
+    return knex.select({uid: 'group'}).from('group_message_relationships')
+        .where('message', messageID).whereIn('status', ['host', 'publish']);
+
+}
+
+export async function getMessageGroupRecipients(user, messageID){
+    return knex.select({uid: 'group'}).from('group_message_relationships')
+        .where('message', messageID).where('status', 'recieve');
+}
+
+/**
+ * @summary Renvoie simplement un groupe en fonction de son identifiant.
+ * @param {Object} user - Utilisateur effectuant la requête.
+ * @param {String} groupUID - Identifiant unique du groupe.
+ * @author manifold 
+ * @rights super
+ */
+export async function getGroup(gid : string) : Promise<Group>{
+    // Une sélection sur une table renvoie un tableau.
+    // Knex renvoie une promesse, qui se résout en le tableau sélectionné.
+    // On récupère son unique valeur, puisqu'on filtre sur l'identifiant unique.
+    return knex.select().from('groups').where('uid',gid).then(results => results[0]);
+};
+
+/**
+ * @summary Renvoie simplement un groupe simple en fonction de son identifiant.
+ * @param {Object} user - Utilisateur effectuant la requête.
+ * @param {String} groupUID - Identifiant unique du groupe.
+ * @author manifold 
+ * @rights super
+ */
+export async function getSimpleGroup(gid : string) : Promise<Group>{
+    return knex.select().from('simple_groups').where('uid', gid).then(results => results [0]);
+};
+
+/**
+ * @summary Renvoie simplement un meta groupe en fonction de son identifiant.
+ * @param {Object} user - Utilisateur effectuant la requête.
+ * @param {String} groupUID - Identifiant unique du groupe.
+ * @author manifold 
+ * @rights super
+ */
+export async function getMetaGroup(gid : string) : Promise<Group>{
+    return knex.select().from('meta_groups').where('uid', gid).then(results => results [0]);
+};
+
+/**
+ * @summary Refuse une requête d'un groupe voulant rejoindre un évènement
+ * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. 
+ * @arg {Int} requestID - L'id de la requête à refuser. 
+ * @return {Promise(Boolean)} Vrai si l'opération a réussie;
+ * @rights admin(request.recipient)
+ */
+export async function denyGroupJoinEventRequest(requestID){
+    await knex('group_join_event').where('id', requestID).del();
+    return true;
+}
+
+/**
+ * @summary Refuse une requête d'un groupe voulant rejoindre un évènement
+ * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. 
+ * @arg {Int} requestID - L'id de la requête à refuser. 
+ * @return {Promise(Boolean)} Vrai si l'opération a réussie;
+ * @rights admin(request.recipient)
+ */
+export async function acceptGroupJoinEventRequest(user, requestID){
+    let request = await knex('group_join_event').select().where('id', requestID);
+    if( !request)
+        return false;
+    await knex('group_join_event').where('id', requestID).del();
+    let group = request[0].senderuid;
+    let event = request[0].eventuid;
+    await knex('group_participation').insert({
+        group : group,
+        message : event,
+        status : "join"
+    });
+    return;
+
+}
+
+
+/**
+ * @summary Refuse une requête d'un groupe voulant rejoindre un évènement
+ * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. 
+ * @arg {Int} requestID - L'id de la requête à refuser. 
+ * @return {Promise(Boolean)} Vrai si l'opération a réussie;
+ * @rights admin(request.recipient)
+ */
+export async function denyYourGroupHostEventRequest(requestID : string) : Promise<boolean>{
+    await knex('your_group_host_event').where('id', requestID).del();
+    throw new Error('Not implemented');
+}
+
+/**
+ * @summary Refuse une requête d'un groupe voulant rejoindre un évènement
+ * @arg {Object} user - Représente l'utilisateur qui a effectué la requête. 
+ * @arg {Int} requestID - L'id de la requête à refuser. 
+ * @return {Promise(Boolean)} Vrai si l'opération a réussie;
+ * @rights admin(request.recipient)
+ */
+export async function acceptYourGroupHostEventRequest(requestID : string) : Promise<Boolean>{
+    let request = await knex('your_group_host_event').select().where('id', requestID);
+    if( !request)
+        return false;
+    await knex('group_join_event').where('id', requestID).del();
+    let group = request[0].recipient;
+    let event = request[0].eventuid;
+    await knex('group_message_relationships').insert({
+        group : group,
+        message : event,
+        status : "host"
+    });
+    return;
+
+}
+
+export async function takeAdminRights(user, groupUID, justification) : Promise<boolean>{
+    await knex('taken_rights').insert({
+        user_uid : user.uid,
+        group_uid : groupUID,
+        justification : justification
+    });
+    return true;
+}
+
+
+export function releaseAdminRights(user, groupUID){
+    return knex('taken_rights').del().where('user_uid', user.uid).where('group_uid', groupUID);
+}
+
+/**
+ * @summary Renvoie les membres d'un groupe quelquonque.
+ * @param {Object} user - Utilisateur effectuant la requête.
+ * @param {String} metaGroupUID - Identifiant unique du groupe.
+ * @return {Promise(List)} Une liste des uid de tous les membres du groupe
+ * @author akka vodol 
+ * @rights member(metaGroupUID)
+ */
+export async function getGroupMemberUsers(quser : QUser, group : Group) : Promise<User[]>{
+    const memberSet = await getGroupMemberUsersSet(quser, group);
+    return Array.from(memberSet.values()).map( uid => new User(uid) );
+}
+
+async function getGroupMemberUsersSet(quser : QUser, group : Group) : Promise<Set<string>>{
+    switch( group.type ){
+        case "simple":
+            if(!group.members){
+                await group.fetchData();
+            }
+            return new Set(group.members);
+        case "meta":
+            let member_group_list = await knex.distinct().select().from('groups')
+            .innerJoin('meta_group_membership', 'groups.uid', 'meta_group_membership.member_uid')
+            .where('meta_group_membership.union_uid', '=', group.gid);
+            let members = new Set;
+            for(const memberGroup of member_group_list){
+                let res = await getGroupMemberUsers(quser, new Group(memberGroup));
+                for(const member of res.values()){
+                    members.add(member);
+                }
+            }
+            return members;
+        default:
+            return undefined;
+        }
+}
+
+
+/**
+ * @summary Renvoie les membres d'un groupe quelquonque.
+ * @param {Object} user - Utilisateur effectuant la requête.
+ * @param {String} metaGroupUID - Identifiant unique du groupe.
+ * @return {Promise(List)} Une liste des uid de tous les membres du groupe
+ * @author akka vodol 
+ * @rights member(metaGroupUID)
+ */
+export async function getGroupAdminUsers(quser : QUser, group : Group) : Promise<User[]>{
+    const memberSet = await getGroupAdminUsersSet(quser, group);
+    return Array.from(memberSet.values()).map( uid => new User(uid) );
+}
+
+async function getGroupAdminUsersSet(quser : QUser, group : Group) : Promise<Set<string>>{
+    switch( group.type ){
+        case "simple":
+            if(!group.members){
+                await group.fetchData();
+            }
+            return new Set(group.admins);
+        case "meta":
+            // TODO : Meta group administration not yet implemented
+            return new Set([]);
+        default:
+            return undefined;
+        }
+}
+
+/*
+ * réflexion sur une façon possible de gérer les utilisateurs sans en demander trop à LDAP
+ * Sans utilité pour le moment, ne faites pas attention 
+ */
+
+/*
+function smartUserObject(user, uid){
+    this.user = user;
+    this.uid = uid;
+
+    this.resolution = {};
+    let resolutionAlias = this.resolution;
+
+    let attributeDictionnary = {
+        givenName : "givenName"
+    };
+
+    for(let attribute in attributeDictionnary){
+        this[attribute] = function(){
+            return new Promise((resolve, reject) => {
+                resolutionAlias[attributeDictionnary[attribute]] = resolve;
+            });
+        };
+    }
+
+    this.resolve = async function(){
+        let userObject = await renseignerSurUtilisateur(this.user, this.uid);
+        for(let attribute in this.resolution){
+            this.resolution[attribute](userObject.attribute);
+        }
+    };
+}
+*/
\ No newline at end of file
diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts
index 8d44672f7bde4c5803aec19077e1d8c8d94f4b07..9256a5377aa7f9501ae4a5b2d1933612c1c0b9d9 100644
--- a/src/graphql/resolvers.ts
+++ b/src/graphql/resolvers.ts
@@ -8,8 +8,9 @@ import knex from '../../db/knex_router';
 
 import '../config_passport';
 
-import * as connectors from './connectors/connectors';
-import * as authentifiers from './connectors/authentifiers';
+import * as auth from './new_connectors/authorisation';
+import * as conn from './new_connectors/connection';
+
 import MessageResolvers from './resolvers/messages';
 import GroupResolvers from './resolvers/groups';
 
@@ -39,104 +40,72 @@ export const resolvers = {
 
         // group queries
 
-        allGroups: async function(root, args, context){
-            let user = await authentifiers.anonymous(context.user);
-            return user && connectors.getAllVisibleGroups(user);
+        allGroups: async function(root, args, context) : Promise<conn.Group[]>{
+            const visibleGroupSet = await auth.forViewer(context.user);
+            return conn.getGroups(visibleGroupSet);
         },
 
-        allSimpleGroups: async function (root, args, context){
-            let user = await authentifiers.anonymous(context.user);
-            return user && connectors.getAllVisibleSimpleGroups(user);
+        allSimpleGroups: async function (root, args, context) : Promise<conn.Group[]>{
+            const visibleGroupSet = await auth.forViewer(context.user);
+            return conn.getSimpleGroups(visibleGroupSet);
         },
 
-        group: async function(root, args, context) {
-            let user = await authentifiers.anonymous(context.user);
-            return user && connectors.getGroupIfVisible(user, args.uid);
+        allMetaGroups: async function(root, args, context) : Promise<conn.Group[]>{
+            const visibleGroupSet = await auth.forViewer(context.user);
+            return conn.getMetaGroups(visibleGroupSet);
         },
 
-        simpleGroup: async function(obj, args, context){
-            let user = await authentifiers.anonymous(context.user);
-            return user && connectors.getSimpleGroupIfVisible(user, args.uid);
+        group: async function(root, args, context) : Promise<conn.Group>{
+            if(await auth.isViewer(context.user, args.gid)){
+                return conn.getGroup(args.gid);
+            }else{
+                return null;
+            }
         },
-        metaGroup: async function(obj, args, context){
-            let user = await authentifiers.anonymous(context.user);
-            return user && connectors.getMetaGroupIfVisible(user, args.uid);
+
+        simpleGroup: async function(obj, args, context) : Promise<conn.Group>{
+            if(await auth.isViewer(context.user, args.gid)){
+                return conn.getSimpleGroup(args.gid);
+            }else{
+                return null;
+            }
+        },
+
+        metaGroup: async function(obj, args, context) : Promise<conn.Group>{
+            if(await auth.isViewer(context.user, args.gid)){
+                return conn.getMetaGroup(args.gid);
+            }else{
+                return null;
+            }
         },
 
         /*
          * Message queries.
          */
 
-        allAnnouncements: function(obj, args, context) {
-            return knex.select().from("announcements");
+        allAnnouncements: async function(obj, args, context) : Promise<Object[]> {
+            const visibleGroupSet = await auth.forViewer(context.user);
+            return conn.allAnnouncements(visibleGroupSet);
         },
 
-        allEvents(root, args, context) {
-            return knex.select().from("events");
-        },
-
-        allMessages(root, args, context) {
-            const events = knex.select().from("events");
-            const posts = knex.select().from("posts");
-            return Promise.all([events, posts]).then(res => {
-                return _.flatten(res);
-            });
+        allEvents: async function (root, args, context) : Promise<Object[]> {
+            const visibleGroupSet = await auth.forViewer(context.user);
+            return conn.allAnnouncements(visibleGroupSet);
         },
 
-
         // user queries
 
-        user: async function(obj, args, context){
-            let user = await authentifiers.anonymous(context.user);
-            return user && connectors.getUser(user,args.uid);
-        },
-
-        searchTOL: (obj, args, context) => {
-            const result = connectors.utilisateur.repliquerTOLdesIds({
-                givenName: args.givenName,
-                lastName: args.lastName,
-                nickname: args.nickname,
-                nationality: args.nationality,
-                school: args.school,
-                promotion: args.promotion,
-                groups: args.groups,
-                studies: args.studies,
-                sport: args.sport,
-                phone: args.phone,
-                mail: args.mail,
-                adress: args.adress,
-                ip: args.ip
-            });
-            return result;
-        },
-
-        // viewer queries
-
-        // member queries
-
-        allMembers : async function(obj, args, context){
-            let user = await authentifiers.member(context.user, args.from);
-            return user && connectors.getGroupMemberUsers(context.user, obj.groupUID);
+        user: async function(root, args, context){
+            return conn.getUser(args.uid);
         },
 
-        // speaker queries
-
-        allRequests: async function(obj, args, context){
-            let res = [];
-            let user = authentifiers.admin(context.user, args.from);
-            if(user){
-                res = res.concat(await connectors.getUserJoinGroupRequests(user, args.from));
-            }
-            user = user || authentifiers.speaker(user, args.from);
-            if(user){
-                res = res.concat(await connectors.getGroupJoinEventRequests(user, args.from));
-                res = res.concat(await connectors.getYourGroupHostEventRequests(user, args.from));
-            }
-            return res;
+        searchTOL: (root, args, context) => {
+            // TODO : filter
+            return conn.searchUsers(args);
         },
 
-        test: async function(obj, args, context){
-            return connectors.getSimpleGroup(context.user, "br");
+        test: async function(root, args, context){
+            return conn.getSimpleGroup("br");
         }
     },
 
@@ -150,37 +119,27 @@ export const resolvers = {
     // @rights admin(obj.groupUID)
     UserJoinGroup: {
         user : (obj, args, context) => {
-            return connectors.getUser(context.user, obj.useruid);
-            /*return connectors.getUser(context.user, "quentin.gendre");
-            if(obj.useruid === "anatole.romon"){
-                return connectors.getUser(context.user, "anatole.romon").then(res => {
-                    return connectors.getUser(context.user, "quentin.gendre");
-                });
-            }else{
-                return new Promise( (resolve, reject) => {
-                    resolve({givenName : "patrick"});
-                });
-            }*/
+            return conn.getUser(obj.useruid);
         }
     },
 
     // @rights speaker(obj.groupUID)
     GroupJoinEvent : {
         event: (obj, args, context) => {
-            return connectors.getEvent(context.user, obj.eventuid);
+            return conn.getEvent(obj.eventuid);
         },
         groupWantingToJoin: (obj, args, context) => {
-            return connectors.getGroup(context.user, obj.senderuid);
+            return conn.getGroup(obj.senderuid);
         }
     },
 
     // @rights speaker(obj.groupUID)
     YourGroupHostEvent : {
         event: (obj, args, context) => {
-            return connectors.getEvent(context.user, obj.eventuid);
+            return conn.getEvent(obj.eventuid);
         },
         sender: (obj, args, context) => {
-            return connectors.getGroup(context.user, obj.senderuid);
+            return conn.getGroup(obj.senderuid);
         }
     },
 
@@ -188,7 +147,7 @@ export const resolvers = {
     User : {
         groups : (obj, args, context) => {
             let result = Promise.all(obj.groups.map((grid) => {
-                return connectors.getSimpleGroup(context.user,grid);
+                return conn.getSimpleGroup(grid);
             }));
 
             return result.then(groups => {
@@ -203,21 +162,23 @@ export const resolvers = {
 
         // Superviser mutations
 
-        takeAdminRights : async function(obj, args, context){
-            let user = await authentifiers.superviser(context.user, args.from);
-            return user && await connectors.takeAdminRights(user, args.from, user.justification);
+        takeAdminRights : async function(obj, args, context) : Promise<boolean>{
+            const justification = await auth.isSupervisor(context.user, args.from);
+            if(justification){
+                return conn.takeAdminRights(context.user, args.from, justification);
+            }else{
+                return false;
+            }
         },
 
         releaseAdminRights : async function(obj, args, context){
-            let user = await authentifiers.superviser(context.user, args.from);
-            return user && await connectors.releaseAdminRights(user, args.from, user.justification);
+            await conn.releaseAdminRights(context.user, args.from);
         },
 
         // Admin mutations
 
         createSubgroup: async function (obj, args, context){
-            let user = authentifiers.admin(context.user, args.from);
-            return user && connectors.createSubgroup(user, args);
+            throw new Error('Not implemented');
         },
     },
 
diff --git a/src/graphql/typeDefs/actions.graphql b/src/graphql/typeDefs/actions.graphql
index 18fe6cf9786bb589aebfbda139ae4356142d5b97..136919d6fa02e1666af2e55bcd9b67a2bafa50fe 100644
--- a/src/graphql/typeDefs/actions.graphql
+++ b/src/graphql/typeDefs/actions.graphql
@@ -6,13 +6,12 @@ type Query {
     allGroups: [Group]
     allSimpleGroups: [SimpleGroup]
 
-    group(uid: ID!) : Group
-    simpleGroup(uid : ID!) : SimpleGroup
-    metaGroup(uid : ID!) : MetaGroup
+    group(gid: ID!) : Group
+    simpleGroup(gid : ID!) : SimpleGroup
+    metaGroup(gid : ID!) : MetaGroup
 
     # message queries
 
-    allMessages: [Message]
     message(id: ID!): Message
     allEvents: [Event]
     allAnnouncements: [Announcement]
@@ -37,20 +36,6 @@ type Query {
         ip: String
     ): [String]
 
-    # Admin queries
-
-    allRequests(from : String!) : [Request]
-
-    # Speaker queries 
-
-    # allRequests(from : String!) : [Request]
-
-    # Member Queries
-
-    allMembers(from : String!) : [Group]
-
-    # Viewer Queries
-    
     test : String
 }
 
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..7f70480af96d0a018ca776779806754738cf5c3d
--- /dev/null
+++ b/src/graphql/typeDefs/objects_ldap.graphql
@@ -0,0 +1,244 @@
+# hawkspar->all ; doc ?
+# kadabra -> hawkspar : wtf c'est quoi ce schéma qui sort de nulle part ? est-ce qu'il sert à quelque chose ?
+
+# 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/export/group.ts b/src/ldap/export/group.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e7cba6a318342ad2ac905f2b00dd7c4c93f5987c
--- /dev/null
+++ b/src/ldap/export/group.ts
@@ -0,0 +1,386 @@
+/**
+ * @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 '../internal/config';
+import {LDAP} from '../internal/basics';
+import {Tools} from '../internal/utilities';
+
+/**
+ * @interface groupData
+ * @var {string} gid - Identifiant du groupe
+ * @var {string} name - Nom du groupe (souvent son nom mais pas nécessairement)
+ * @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.peek} 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 au format {@link groupData}.
+     * @static
+     * @async
+     */
+    static async peek(gid: string) : Promise<groupData> {
+        try {
+            return Tools.peek<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) pour les groupes.
+     * @desc Cette fonction utilise {@link Tools.search}.
+     * @arg {groupData} 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(data: groupData) : Promise<string[]> {
+        try {
+            return Tools.search("gr", data);
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche approximative d'un groupe.";
+        }
+    }
+
+    /**
+     * @summary Fonction qui permet d'ajouter un utilisateur à un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Tools.getMembers}, {@link Tools.getGroups} et {@link LDAP.change}.
+     * @arg {string} uid - Identifiant de l'utilisateur à ajouter
+     * @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 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 Tools.getMembers}, {@link Tools.getGroups} et {@link LDAP.change}.
+     * @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) {
+                        Group.addMember(id, gid).then(res => {
+                            if (!res) { throw "Erreur lors du ré-ajout d'un autre membre"; }
+                        });
+                    }
+                });
+            }
+        }
+        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) {
+                        Group.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 un membre au stade d'administrateur d'un groupe.
+     * @desc Cette fonction fait essentiellement appel à {@link Group.addMember} {@link Tools.getAdmins} et {@link LDAP.change}. Elle n'autorise pas
+     * les doublons et opère dans les deux dns users et groups.
+     * @arg {string} uid - Identifiant du membre futur admin
+     * @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 Group.remMember}, {@link Group.addMember} {@link LDAP.change}.
+     * Rajoute l'utilisateur au groupe par effet de bord si l'utilisateur n'est pas administrateur.
+     * @arg {string} uid - Identifiant de l'admin à dégrader, supposé 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 de l'ex-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 Group.addMember} et {@link Group.addAdmin}
+     * 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. A modifier une fois que le LDAP incluerait les groupes administres par une utilisateur.
+     * Appelle {@link LDAP.clear} bien sûr, mais aussi {@link Group.remMember} et {@link Group.remAdmin} 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: string): Promise<boolean> {
+        try {
+            // Gestion des administrateur et membres d'abord
+            let profil = await Group.peek(gid);
+            // Modification du profil de chaque utilisateur
+            profil[ldapConfig.group['member']].forEach(async function quickPartRemUser(uid: string) {
+                // Modification des profils de tous les utilisateurs
+                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) {
+                            Group.addMember(uid, id).then(res => {
+                                if (!res) { throw "Erreur lors du ré-ajout des autres groupes"; }
+                            });
+                        }
+                    });
+                }
+            });
+            // 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. Sans influence sur ses membres ou admins. 
+     * @desc Appelle {@link Tools.edit}.
+     * @arg {groupData} data - Dictionnaire des informations du groupe.
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async edit(data: groupData) : Promise<boolean> {
+        try {
+            return Tools.edit("gr",data);
+        }
+        catch(err) {
+            throw "Erreur lors de la modification d'un groupe.";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/ldap/export/user.ts b/src/ldap/export/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cac82805916a4a067dd7141aa3e593b6af625a83
--- /dev/null
+++ b/src/ldap/export/user.ts
@@ -0,0 +1,273 @@
+/**
+ * @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 '../internal/config';
+import {LDAP} from '../internal/basics';
+import {Tools} from '../internal/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.peek} avec l'interface {@link userData}.
+     * @arg {string} uid - Identifiant de l'utilisateur, supposé valide.
+     * @return {Promise(userData)} Informations recueillies au format {@link userData}.
+     * @static
+     * @async
+     */
+    static async peek(uid: string) : Promise<userData> {
+        try { 
+            return Tools.peek<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 User.peek} au cas par cas après pour obtenir les vraies infos.
+     * @desc Cette fonction utilise {@link Tools.search}.
+     * @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 exemple pour chercher un membre
+     * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined.
+     * @return {Promise(string[])} gids des profils qui "match" les critères proposés.
+     * @static
+     * @async
+     */
+    static async search(data: userData) : Promise<string[]> {
+        try {
+            return Tools.search("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 Group.addMember} et {@link Group.addAdmin} 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.
+     * Cette application ne permet pas de rejoindre des groupes.
+     * @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."; }
+            });
+        });
+        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 Group.remMember} et {@link Group.remAdmin} 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(async function (gid: string) {
+                // Si l'utilisateur était admin, l'enlever
+                Group.remAdmin(uid, gid);
+                // Enlever de la liste des membres
+                let lm = await Tools.getMembers(gid);
+                if (lm.includes(uid)) {
+                    // Supprime tous les membres
+                    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) {
+                            Group.addMember(id, gid).then(res => {
+                                if (!res) { throw "Erreur lors du ré-ajout d'un autre membre"; }
+                            });
+                        }
+                    });
+                }
+            });
+        }
+        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.
+     * @desc Appelle simplement {@link Tools.edit}. Sans effet sur les groupes de l'utilisateur concerné.
+     * @arg {userData} data - Dictionnaire des informations utilisateurs
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon
+     * @async
+     * @static
+     */
+    static async edit(data : userData) : Promise<boolean> {
+        try {
+            return Tools.edit("us",data);
+        }
+        catch(err) {
+            throw "Erreur lors de la modification d'un utilisateur.";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/ldap/basics.ts b/src/ldap/internal/basics.ts
similarity index 58%
rename from src/ldap/basics.ts
rename to src/ldap/internal/basics.ts
index 9e29669db22816ed92d17484f6f213192aa79d5f..8135f1aa4b2bec05a2a5d981987336bd29db257f 100644
--- a/src/ldap/basics.ts
+++ b/src/ldap/internal/basics.ts
@@ -19,6 +19,10 @@ import {ldapConfig, credentialsLdapConfig} from './config';
 // Connection au serveur LDAP avec des temps de timeout arbitraires
 var client = ldap.createClient({ url: ldapConfig.server});
 
+interface dic {
+    [Key: string]: string;
+}
+
 //------------------------------------------------------------------------------------------------------------------------
 // Fonctions de base agissant sur le LDAP
 //------------------------------------------------------------------------------------------------------------------------
@@ -71,23 +75,73 @@ export class LDAP {
      * @async
      */
     static async unbind() : Promise<boolean> { return LDAP.bind("", ""); }
-
+    
     /**
-     * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit les valeurs trouvées.
+     * @summary Fonction qui interroge le LDAP selon un protocole spécifié en argument et renvoit une liste de 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 {'gr'|'us'} domain - Emplacement de la requête (groupe ou utilisateur)
+     * @arg {string} attribute - Attribut unique à renvoyer
+     * @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(string[])} 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 searchSingle(domain: 'gr'|'us', attribute: string, id: string=null, filter: string="(objectClass=*)") : Promise<string[]> {
+        LDAP.adminBind();
+        let dn ="";
+        if (id != null)     { dn+=ldapConfig.key_id+'='+id+','; }
+        if (domain == "gr") { dn+=ldapConfig.dn_groups; }
+        else                { dn+=ldapConfig.dn_users; }
+        let vals=[];
+        // Interrogation LDAP selon filter
+        client.search(ldapEscape.dn("${txt}", { txt: dn}), {
+            "scope": "sub",
+            "filter": ldapEscape.filter("${txt}", { txt: filter}),
+            "attributes": [attribute]
+        }, (err, res) => {
+            // Gestion erreur ; pb car pas simple true / autre en sortie
+            if (err) {
+                throw "Erreur lors de la recherche sur le LDAP.";
+            } else {
+                // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse
+                res.on('searchEntry', entry => {
+                    // Cas un seul attribut où le résultat est une liste directement
+                    vals.push(entry.object[attribute]);
+                });
+                // Si la recherche renvoie une erreur, on renvoit
+                res.on('error', resErr => { throw resErr; });
+                // Si la recherche est finie on se déconnecte
+                res.on('end', _ => { LDAP.unbind(); });
+            }
+        });
+        // On renvoit le résultat
+        return vals;
+    }
+    
+    /**
+     * @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 {'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
-     * @return {(Promise(Array.<Object>)|Promise(Array.Object.<string, Object>))} Résultats de la recherche ; soit une liste de valeurs d'attributs, 
+     * @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<dic>)} 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 searchMultiple(domain: 'gr'|'us', attributes: string[], id: string=null, filter: string="(objectClass=*)") : Promise<Array<dic>> {
         LDAP.adminBind();
+        let dn ="";
+        if (id != null)     { dn+=ldapConfig.key_id+'='+id+','; }
+        if (domain == "gr") { dn+=ldapConfig.dn_groups; }
+        else                { dn+=ldapConfig.dn_users; }
         let vals=[];
-        // Interrogation LDAP selon ldapConfiguration fournie en argument
+        // Interrogation LDAP selon filter
         client.search(ldapEscape.dn("${txt}", { txt: dn}), {
             "scope": "sub",
             "filter": ldapEscape.filter("${txt}", { txt: filter}),
@@ -99,23 +153,19 @@ export class LDAP {
             } else {
                 // Dès que la recherche renvoit une entrée, on stocke les attributs qui nous intéresse
                 res.on('searchEntry', entry => {
-                    // Cas un seul attribut où le résultat est une liste directement
-                    if (!Array.isArray(attributes)) {  vals.push(entry.object[attributes]); }
-                    else if (attributes.length == 1) { vals.push(entry.object[attributes[0]]); }
                     // Cas plusieurs attributs donc résultat dictionnaire
-                    else {
-                        vals.push({});
-                        attributes.forEach(attribute => {
-                            vals.slice(-1)[0][attribute]=entry.object[attribute];
-                        });
-                    }
+                    vals.push({});
+                    attributes.forEach(attribute => {
+                        vals.slice(-1)[0][attribute]=entry.object[attribute];
+                    });
                 });
                 // 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,20 +173,24 @@ 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.
+     * @arg {dic} mod - Dictionnaire contenant les attributs à modifier et les nouvelles valeurs des attributs.
+     * @arg {string} mod[key] - Nouvelle valeur de l'attribut key. Une nouvelle valeur vide ("") est équivalent à la suppression de cet attribut.
      * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, `false` sinon.
      * @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: dic) : Promise<boolean> {
         LDAP.adminBind();
-        // Modification LDAP selon ldapConfiguration en argument (pourrait prendre une liste de Changes)
+        let dn = ldapConfig.key_id+'='+id+','
+        if (domain == "gr") { dn+=ldapConfig.dn_groups }
+        else                { dn+=ldapConfig.dn_users }
+        // Modification LDAP selon dn fourni 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,17 +204,20 @@ 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();
+        let dn = ldapConfig.key_id+"="+vals[ldapConfig.key_id];
+        if (domain == "gr") { dn+=ldapConfig.dn_groups; }
+        else                { 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 => {
+        client.add(ldapEscape.dn("${txt}", { txt: dn}), vals, err => {
             throw "Erreur lors d'une opération d'ajout sur le LDAP.";
         });
         LDAP.unbind();
@@ -172,13 +229,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/config.ts b/src/ldap/internal/config.ts
similarity index 84%
rename from src/ldap/config.ts
rename to src/ldap/internal/config.ts
index a05c02d720f3d5bb5188d273d76fd53df4a34034..d1922ecca2eea3d5f4c8c3e4b92d4921908528dd 100644
--- a/src/ldap/config.ts
+++ b/src/ldap/internal/config.ts
@@ -7,11 +7,11 @@ import fs from 'fs';
 import path from 'path';
 import colors from 'colors';
 // Point central ; tous les champs de la BDD sont 'cachés' dans config.json et pas visibles directement
-let path_config = path.resolve('ldap_config.json');
-let path_credentials = path.resolve('ldap_credentials.json');
+let path_config = path.resolve(__dirname,'..','ldap_config.json');
 console.log(colors.cyan("Loading LDAP config file from "+path_config));
-console.log(colors.cyan("Loading LDAP credentials from "+path_credentials));
 export const ldapConfig = JSON.parse(fs.readFileSync(path_config).toString());
+let path_credentials = path.resolve(__dirname,'..','ldap_credentials.json')
+console.log(colors.cyan("Loading LDAP credentials from "+path_credentials));
 export const credentialsLdapConfig = JSON.parse(fs.readFileSync(path_credentials).toString());
 // Override config server from environment
 if (process.env.LDAP_URI != null) {
diff --git a/src/ldap/internal/utilities.ts b/src/ldap/internal/utilities.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f4b9613cde1dee830097724f6f7a749af1b801e1
--- /dev/null
+++ b/src/ldap/internal/utilities.ts
@@ -0,0 +1,315 @@
+/**
+ * @file Ce fichier regroupe les fonctions simples de recherche et de test utiles, mais trop puissantes pour être exportées directement.
+ * 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 {userData} from '../export/user';
+import {groupData} from '../export/group';
+
+//------------------------------------------------------------------------------------------------------------------------
+// Fonctions intermédiaires TBT
+//------------------------------------------------------------------------------------------------------------------------
+
+export class Tools {
+    /**
+     * @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 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 peek<T>(domain: 'us'|'gr', id: string) : Promise<T> {
+        if (domain='gr') {
+            var dirtyKeys = ldapConfig.group;
+        }
+        else {
+            var dirtyKeys = ldapConfig.user;
+        }
+        let cleanData : T;
+        let dirtyData = await LDAP.searchMultiple(domain, dirtyKeys.values(), id)[0];
+        // 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 retrouve les id des paxs ou groupes validant les critères de recherche. Etape vers vrai TOL (Trombino On Line).
+     * Utiliser {@link peekUser} au cas par cas après pour obtenir les vraies infos.
+     * @desc Cette fonction utilise {@link LDAP.search} mais avec un filtre généré à la volée. Accepte des champs exacts ou incomplets pour la plupart des champs
+     * mais pas approximatifs et ne gère pas l'auto-complete. MEF Timeout pour des recherches trop vagues. Va crasher si un champ n'est pas dans ldapConfig.
+     * @param T - Format renvoyé (en pratique {@link userData} ou {@link groupData})
+     * @arg {"us"|"gr"} domain - Domaine de la recherche (utilisateur ou groupe)
+     * @arg {userData | groupData} data - Dictionnaire contenant les données nécessaires à la recherche. Les valeurs sont celles entrées par l'utilisateur et sont par hypothèse
+     * comme des sous-parties compactes des valeurs renvoyées. Tous les champs ci-dessous peuvent être indifféremment des listes (par exemple pour chercher un membre
+     * de plusieurs groupes) ou des éléments isolés. Si un champ n'est pas pertinent, le mettre à '' ou undefined.
+     * @return {Promise(string[])} ids des profils qui "match" les critères proposés.
+     * @static
+     * @async
+     */
+    static async search(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]]; }          // Génération systématique d'une liste de valeurs à rechercher
+                // Iteration pour chaque valeur fournie par l'utilisateur
+                data[key].forEach(val => {
+                    // Traduction en language LDAP
+                    let attribute = "";
+                    if (domain="us")    { attribute = ldapConfig.user[key]; }
+                    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)
+                                        "(|("+attribute+"=*"+val+"*)"+     // La valeur du milieu avec des trucs avant et après
+                                        "("+  attribute+"="+ val+"*)))))"; // La valeur du début avec des trucs après
+                });
+            }
+        }
+        // Appel avec filtre de l'espace 
+        return LDAP.searchSingle(domain, ldapConfig.key_id, null, filter);
+    }
+    
+    /**
+     * @summary Fonction qui édite un groupe ou utilisateur existant dans le LDAP. N'agit pas sur l'apprtenance à un groupe.
+     * @desc Appelle {@link LDAP.change}.
+     * @arg {"us"|"gr"} domain - Domaine de l'opération' (utilisateur ou groupe).
+     * @arg {userData | groupData} data - Dictionnaire avec les nouvelles valeurs de la feuille.
+     * @return {Promise(boolean)} `true` si la modification s'est bien déroulée, false sinon.
+     * @async
+     * @static
+     */
+    static async edit(domain: "us"|"gr", data: userData|groupData) : Promise<boolean> {
+        if (domain = "us") {
+            var id=data['uid'];
+            var dirtyKeys=ldapConfig.user;
+        }
+        else {
+            var id=data['gid'];
+            var dirtyKeys=ldapConfig.group;
+        }
+        // Rename in an LDAP-friendly way
+        let dirtyData = {};
+        Object.keys(data).forEach(function(key: string) {
+            // Some values edit can't change
+            if (!['readPerm','writePerm','groups','groupsIsAdmin','members','admins'].includes(key)) {
+                dirtyData[dirtyKeys.key]=data[key];
+            }
+        });
+        return LDAP.change(domain,id,"replace",dirtyData);
+    }
+
+    /**
+     * @callback changeValueCallback
+     * @param {string} id - Id à modifier
+     * @param {number} n - Nombre d'itérations
+     * @return {string} Nouveau id
+     */
+    /**
+     * @summary Cette fonction teste une valeur d'un attribut (typiquement un identifiant) et le fait évoluer jusqu'à ce qu'il soit unique.
+     * @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.search} pour vérifier 
+     * qu'il n'y a pas d'autres occurences de cette valeur pour cette attribut
+     * 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 {"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 {number} n [0] - Nombre d'itérations (à initialiser à 0)
+     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
+     * @static
+     * @async
+     */
+    static async ensureUnique(value: string, attribute: string, domain: 'gr'|'us', changeValue: (string, number) => string, n: number=0) : Promise<string> {
+        // Recherche d'autres occurences de l'id
+        try {
+            return LDAP.searchSingle(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; }
+                // Sinon, on tente de nouveau notre chance avec la valeur suivante
+                else { return Tools.ensureUnique(changeValue(value, n+1), attribute, domain, changeValue, n+1); }
+            });
+        }
+        catch(err) {
+            throw "Erreur lors de la recherche d'une valeur pour assurer son unicité.";
+        }
+    }
+
+    /**
+     * @summary Cette fonction génère un uid standard, puis le fait évoluer jusqu'à ce qu'il soit unique.
+     * @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
+     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
+     * @static
+     * @async
+     */
+    static async generateUid(givenName: string, lastName: string, promotion: string) : Promise<string> {
+        try {
+            // normalize et lowerCase standardisent le format
+            return Tools.ensureUnique((givenName+'.'+lastName).toLowerCase().normalize('UFD'), ldapConfig.key_id, "us", (id: string, n: number) => {
+                if (n=1) { id+='.'+promotion; }                // Si prénom.nom existe déjà, on rajoute la promo
+                else if (n=2) { id+='.'+(n-1).toString(); }    // Puis si prénom.nom.promo existe déjà on passe à nom.prenom.promo .1
+                else if (n>2) { id+=n; }                        // Ensuite on continue .123, .1234, etc...
+                return id;
+            });
+        }
+        catch(err) {
+            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
+        }
+    }
+
+    /**
+     * @summary Cette fonction génère un id lisible, puis le fait évoluer jusqu'à ce qu'il soit unique.
+     * @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
+     * @async
+     */
+    static async generateReadableId(name: string) : Promise<string> {
+        try {
+            // normalize et lowerCase standardisent le format
+            return Tools.ensureUnique(name.toLowerCase().normalize('UFD'), ldapConfig.key_id, "gr", (id: string, n: number) => {
+                if (n=1)        { id+='.'+n.toString(); }   // Si nom existe déjà, on essaie nom.1
+                else if (n>1)   { id+=n.toString(); }       // Ensuite on continue .12, .123, etc...
+                return id;
+            });
+        }
+        catch(err) {
+            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
+        }
+    }
+
+    /**
+     * @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 {"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, domain: "gr"|"us") : Promise<string> {
+        try {
+            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.searchSingle("us", ldapConfig.user.groups, uid);
+        }
+        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.searchSingle("gr", ldapConfig.group.members, gid);
+        }
+        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.searchSingle("gr", ldapConfig.group.admins, gid);
+        }
+        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
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
deleted file mode 100644
index 19b5606d3ab0a625e000656e11850e3518096d1f..0000000000000000000000000000000000000000
--- a/src/ldap/utilities.ts
+++ /dev/null
@@ -1,238 +0,0 @@
-/**
- * @file Ce fichier regroupe les fonctions simples de recherche et de test utiles, mais trop puissantes pour être exportées directement.
- * Le découpage par fichier est arbitraire mais permet de regrouper certaines classes proches.
- * @author hawkspar
- */
-
-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
-}
-
-//------------------------------------------------------------------------------------------------------------------------
-// Fonctions de recherche
-//------------------------------------------------------------------------------------------------------------------------
-
-export class SmartSearch {
-    /**
-     * @class Cette classe contient des fonctions de recherche génériques trop puissantes pour être exportées tel quel.
-     * @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
-     * @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);
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche intelligente d'un groupe.";
-        }
-    }
-
-    /**
-     * @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).
-     * @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
-     * 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.
-     * @static
-     * @async
-     */
-    static async users(data: searchUserFields, return_attributes: string[]) : 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
-                // Iteration pour chaque valeur fournie par l'utilisateur
-                data[key].forEach(val => {
-                    // Traduction en language LDAP
-                    let attribute = ldapConfig.user[key];
-                    // Creation incrémentale du filtre
-                    filter="(&"+filter+ "(|("+attribute+"="+ val+")"+      // On cherche la valeur exacte
-                                        "(|("+attribute+"=*"+val+")"+      // La valeur finale avec des trucs avant ; wildcard * (MEF la wildcart ne marche pas pour tous les attributs)
-                                        "(|("+attribute+"=*"+val+"*)"+     // La valeur du milieu avec des trucs avant et après
-                                        "("+  attribute+"="+ val+"*)))))"; // La valeur du début avec des trucs après
-                });
-            }
-        }
-        // Appel avec filtre de l'espace 
-        try {
-            return LDAP.search(ldapConfig.dn_users, return_attributes, filter);
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche intelligente d'un utilisateur.";
-        }
-    }
-}
-
-//------------------------------------------------------------------------------------------------------------------------
-// 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
-     * @param {string} id - Id à modifier
-     * @param {number} n - Nombre d'itérations
-     * @return {string} Nouveau id
-     */
-    /**
-     * @summary Cette fonction teste une valeur d'un attribut (typiquement un identifiant) et le fait évoluer jusqu'à ce qu'il soit unique.
-     * @desc Librement adapté de Stack Overflow. Appelle {@link LDAP.search} pour vérifier 
-     * qu'il n'y a pas d'autres occurences de cette valeur pour cette attribut
-     * 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 {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)
-     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
-     * @static
-     * @async
-     */
-    static async ensureUnique(value: string, attribute: string, dn: string, 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[]) {
-                if (!matches) { throw ""; }
-                // On renvoit la valeur si elle est bien unique
-                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); }
-            });
-        }
-        catch(err) {
-            throw "Erreur lors de la recherche d'une valeur pour assurer son unicité.";
-        }
-    }
-
-    /**
-     * @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).
-     * @param {string} givenName - Prénom
-     * @param {string} lastName - Nom
-     * @param {string} promotion - Année de promotion
-     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
-     * @static
-     * @async
-     */
-    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
-                else if (n>2) { id+=n; }                        // Ensuite on continue .123, .1234, etc...
-                return id;
-            });
-        }
-        catch(err) {
-            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
-        }
-    }
-
-    /**
-     * @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).
-     * @param {string} name - Nom
-     * @return {Promise(string)} Valeur unique dans le domaine spécifié de l'attribut spécifié
-     * @static
-     * @async
-     */
-    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 id;
-            });
-        }
-        catch(err) {
-            throw "Erreur lors de l'assurance de l'unicité d'un human readable unique identifier (hruid).";
-        }
-    }
-
-    /**
-     * @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
-     * @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> {
-        try {
-            return this.ensureUnique("0", attribut, dn, (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.";
-        }
-    }
-}
\ No newline at end of file
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000000000000000000000000000000000000..15eb334306f0c1ec4be59c9578c67b8818a0bc3a
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,80 @@
+require('dotenv').config()
+
+const { credentialsConfig } =  require('../src/ldap/config');
+
+process.env.PORT = process.env.TEST_PORT || 3001;
+// The port is changed so that the test suite can be ran without interfering with a running server
+
+require('../build/bundle');
+
+const chai = require('chai');
+
+const expect = chai.expect;
+
+const { GraphQLClient } = require('graphql-request')
+
+const apiEndpoint = `http://localhost:${process.env.PORT || 3001}/graphql`;
+
+const auth = {
+  username : credentialsConfig.dn.split("=")[1].split(",")[0], 
+  password : credentialsConfig.passwd
+};
+
+const testList = require('./testData').testList;
+
+describe('test server API', function () {
+  
+  let client;
+
+  it('Should authentify and initialize the client', async function (){
+
+    const tempClient = new GraphQLClient(apiEndpoint);
+    const query = `
+    mutation {
+      login(username : "${auth.username}", password: "${auth.password}" ) 
+    }
+    `;
+
+    const res = await tempClient.request(query);
+    
+    expect(res).to.have.property('login');
+
+    const token = res.login;
+
+    client = new GraphQLClient(apiEndpoint, {
+      headers: {
+        mode : 'cors',  // Don't know what that does, isn't necessary. Worth looking into
+        credentials : 'include',  // Same
+        authorization: `Bearer ${token}`
+      }
+    });
+
+  });
+
+  it('Should query all groups to test the client', async function (){
+    const query = `
+    query {
+      allGroups {
+        name
+      }
+    }
+    `;
+
+    const res = await client.request(query);
+
+
+    expect(res).to.have.property('allGroups');
+    expect(res.allGroups).to.be.an('array');
+    expect(res.allGroups).to.have.lengthOf.above(3);
+  });
+
+  for(const test of testList){
+
+    it(test.description, async function (){
+      const res = await client.request(test.query);
+      expect(res).to.be.deep.equal(test.result);
+    });
+
+  }
+
+});
diff --git a/test/testData.js b/test/testData.js
new file mode 100644
index 0000000000000000000000000000000000000000..f5a32126ab5754bd0c519edec0c7aa56f1ff720b
--- /dev/null
+++ b/test/testData.js
@@ -0,0 +1,83 @@
+/*
+* This is the list of unit tests which will be ran by the npm test command.
+* To add new unit tests, just write the query you want to test in GraphiQL,
+* then add new entries to the list with the following format :
+*   query : the query you wrote
+*   result : the output returned by GraphiQL
+*   description : what the test does
+*
+*   Tests should be written to work on the current seed
+*   If the seed is modified, the expected result for the tests will have to be changed as well
+*/
+
+exports.testList = [
+    {
+      query : `query{allGroups{name}}`,
+      result: {
+        "allGroups": [
+          { "name": "BR" },
+          { "name": "JTX" },
+          { "name": "Faërix" },
+          { "name": "Bôbar" },
+          { "name": "Kès" },
+          { "name": "Subaïsse" },
+          { "name": "X-Broadway" },
+          { "name": "Å’nologie" },
+          { "name": "Tribunes de l'X" },
+          { "name": "X-Finance" },
+          { "name": "ASK" }
+        ]
+      },
+      description : "Should query all groups"
+    },
+    {
+      query : `
+      query{group(uid : "br"){
+        uid,
+        name,
+        website
+      }}`,
+      result : {
+        "group": {
+          "uid": "br",
+          "name": "BR",
+          "website": "br.binets.fr"
+        }
+      },
+      description : "Should query a single group"
+    },
+    {
+      query : `query{
+        allMessages{
+          id,
+          title,
+          content
+        }
+      }`,
+      result : {
+        "allMessages": [
+          {
+            "id": "11",
+            "title": "Fête de la lune",
+            "content": "La fête de la lune, c'est bientôt dans le grand hall !"
+          },
+          {
+            "id": "12",
+            "title": "Perm BR du mardi soir",
+            "content": "La perm' BR c'est maintenant!"
+          },
+          {
+            "id": "13",
+            "title": "Formation Git",
+            "content": "Aujourd'hui, on va parler du système de contrôle de versions Git, qui est particulièrement utile pour travailler à plusieurs sur des projets informatiques: PSC, code de PI ou de projet de MAP, site binet, quoi que ce soit!"
+          },
+          {
+            "id": "14",
+            "title": "Formation Web",
+            "content": "Envie d'apprendre à faire un site Web en Django ? Alors viens en amphi Sauvy ce jeudi à 20h !"
+          }
+        ]
+      },
+      description : "Should fetch all messages"
+    }
+  ];
\ No newline at end of file