diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bbe28b0f742e39b177283fbe548da988b1438fc..3eb60173875f6eaf1323a3b515f4f91e1a8815d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,15 +36,15 @@ Pour `./db/migrations`, il faut utiliser migrations_v3 (la dernière) (c'est log La structure générale du projet est détaillée ci-dessous ; pas d'inquiétude, la plupart des fichiers .js sont aussi extensivement documentés dans la doc générée par JSDoc (ou plus bas si vous lisez depuis JSDoc). Divers fichiers de configuration à la racine du projet : -- `.eslintignore` : dit à eslint de ne pas linter ces fichiers. -- `.eslintrc.json` : config pour eslint. Le plus susceptible d'être modifié est le champ rules. env spécifie l'environnement, et donc quelle syntaxe est utilisée. -- `.gitignore` : dit à git de ne pas track ces fichiers. -- `package-lock.json` : spécifie à npm les dépendances du projet avec leur version précise. équivalent de pip freeze pour flask/django. -- `package.json` : spécifie à npm les dépendances du projet, mais aussi l'auteur, la license, le repo git... bref tout ce qui peut être intéressant pour [npm](https://www.npmjs.com/package/shitpost). Spécifie aussi les scripts à utiliser quand on utilise la commande `npm run (blablabla)`. +- `.eslintignore` : dit à eslint de ne pas linter ces fichiers +- `.eslintrc.json` : config pour eslint. Le plus susceptible d'être modifié est le champ rules. env spécifie l'environnement, et donc quelle syntaxe est utilisée +- `.gitignore` : dit à git de ne pas track ces fichiers +- `package-lock.json` : spécifie à npm les dépendances du projet avec leur version précise. équivalent de pip freeze pour flask/django +- `package.json` : spécifie à npm les dépendances du projet, mais aussi l'auteur, la license, le repo git... bref tout ce qui peut être intéressant pour [npm](https://www.npmjs.com/package/shitpost). Spécifie aussi les scripts à utiliser quand on utilise la commande `npm run (blablabla)` - `configfile_doc.json` : config pour JSDoc, l'outil de génération automatique de documentation - autres fichiers relatifs à des modules particuliers ou à la gestion de paquets - tsconfig.json : pour typescript - - webpack.config.js + - webpack.config.js : pour Webpack - .gitlab-ci.yml : pour le CI (continuous integration) de gitlab - .gitattributes : ?? - .dockerignore : pour Docker @@ -130,10 +130,16 @@ La gestion des sessions et de la distribution de cookies est gérée par passpor ## Panneau d'administration ("adminview") D'habitude, toutes les requêtes arrivent en HTTP POST : les requêtes GraphQL, et les requêtes de connexion (à /login). +Lorsque le site reçoit des requêtes HTTP GET, on l'interprète comme étant une requête venant du navigateur d'une personne qui souhaite se connecter directement au serveur backend, donc probablement un admin. +L'application principale du backend fait alors appel à une "sous-application", adminview (ceci est possible grâce à Express et son objet express.router). -Lorsque le site reçoit des requêtes HTTP GET, il l'interprète comme étant une requête venant du navigateur d'une personne qui souhaite se connecter directement au serveur backend, donc probablement un admin. +Les identifiants à utiliser sont ceux de Frankiz. L'authentification se fait par le LDAP Frankiz via passport. +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. -L'application principale du backend fait alors appel à une "sous-application", adminview (ceci est possible grâce à Express et son objet express.router). +Par cette interface admin, on a accès : +- à une API REST fait-maison (peut-être encore à débugger), permettant de consulter la base de donnée interne à Sigma, via des requêtes construites avec Knex. `/adminview/db/:table?` +- à GraphQL Voyager, un package qui permet d'afficher une représentation sous forme de graphe du schéma GraphQL. `/adminview/voyager` +- à un lien vers GraphQL Playground, qui permet d'écrire ses propres requêtes GraphQL et de les exécuter. Il est en fait accessible sans authentification du tout, au path `/graphql`. GraphQL Playground est désactivé en production. ## Notes sur javascript @@ -143,6 +149,9 @@ La syntaxe adoptée est JavaScript ES6, un standard moderne (2015) de JavaScript Le standard moderne permet d'importer des dépendances en utilisant le mot-clé `import`, ce que le serveur Node.js ne comprend pas puisque la version 8 de Node ne comprend que le standard ES5 (2009), qui gère les imports avec `require()`. ### `async`/`await` +Il s'agit aussi de mots-clés spécifiques au standard moderne ES6. +https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function +https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await ## Outils de développement diff --git a/README.md b/README.md index 4d2b8b01f2c2d7b4028e4f6f0d9054a291eed677..4e207d13e58a34d8c1de5cdd2583f8336c3470f0 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,120 @@ [](https://gitlab.binets.fr/br/sigma-backend/commits/master) -# Introduction +Sigma - serveur backend +=== Ce dépôt contient le _backend_ de Sigma, le successeur de Frankiz, un site étudiant permettant de gérer les groupes et les étudiants du plateau de Saclay. -Pour obtenir une copie de ce dépôt, clonez-le avec +À terme, ce projet doit tourner sur un serveur du BR et servir d'API à un _frontend_ React *au code séparé et documenté séparément* toute les données nécessaires à son bon fonctionnement (authentification, appartenance à un groupe, droits de visibilité...). Le dépôt pour le serveur front se trouve ici : <https://gitlab.binets.fr/br/sigma-frontend> (on l'appellera indifféremment serveur front, front ou frontend...) - git clone git@gitlab.binets.fr:br/sigma-backend.git +## Obtenir une copie du projet en mode développement -ou `git clone https://gitlab.binets.fr/br/sigma-backend.git`, puis installez les dépendances JavaScript avec `npm install`. +Cloner le dépôt : +```bash +git clone git@gitlab.binets.fr:br/sigma-backend.git +# ou +git clone https://gitlab.binets.fr/br/sigma-backend.git +``` -À terme, ce projet doit tourner sur un serveur du BR et servir d'API à un _frontend_ React *au code séparé et documenté séparément* toute les données nécessaires à son bon fonctionnement (authentification, appartenance à un groupe, droits de visibilité...). Le dépôt pour le serveur front se trouve ici : <https://gitlab.binets.fr/br/sigma-frontend> (on l'appellera indifféremment serveur front, front ou frontend...) +Installer les dépendances (spécifiées dans `package.json`) : +```bash +npm install +``` +Certaines dépendances doivent être installées globalement : +```bash +npm install -g knex +npm install -g webpack +npm install -g eslint +``` -Ce document détaille les différentes dépendances du projet, sa structure générale, détaille un peu plus des éléments sur la base de données et la documentation ; le code est également commenté en détail. +Installer PostgreSQL : +```bash +sudo apt install postgresql +``` +Créer un rôle PostgreSQL "sigma" : +```bash +sudo -u postgres -s +createuser sigma --login --createdb --pwprompt +# penser à répercuter le mot de passe choisi dans `./db/knexfile.js` et `./db/knex_init.js` +``` -## Image Docker +Créer une base de données PostgreSQL "sigma_dev" : +```bash +createdb sigma_dev -U sigma -W +``` +- Si vous n'arrivez pas à vous connecter (`createdb: could not connect to database template1: FATAL: Peer authentication failed for user`) : mettre à jour votre fichier `pg_hba.conf`. + - voir https://stackoverflow.com/questions/1471571/how-to-configure-postgresql-for-the-first-time +- Si vous souhaitez utiliser d'autres noms que "sigma" et "sigma_dev" : ça ne pose pas de problème, il vous faudra simplement modifier `./db/knexfile.js` et `./db/knex_init.js`. -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. +Exécuter les *migrations* et les *seeds* de knex : +```bash +# construire le schéma de la BDD, i.e. définir les tables et leur structure. +knex migrate:latest -Compiler l'image : +# insérer des données de test dans la BDD +knex seed:run +``` - docker build -t sigma-api . +Dire à webpack de build le projet (build le bundle `main.js`) : +```bash +npm run dev # en mode developpement +# ou +npm run build # en mode production +``` -Faire tourner le conteneur : +Lancer un serveur express/node (nodemon en fait) : +```bash +npm run start #see (package.json).scripts +``` +Comme indiqué dans src/index.js, ceci lance un serveur servant l'application express sur le port 3000. - docker run sigma-api +## Déployer dans un conteneur Docker -avec un LDAP custom : +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. - docker run -e LDAP_URI=ldap://172.17.0.1:8389 sigma-api +Compiler l'image : +```bash +docker build -t sigma-api . +``` +Faire tourner le conteneur : +```bash +docker run sigma-api +``` +Idem mais avec un LDAP custom : +```bash +docker run -e LDAP_URI=ldap://172.17.0.1:8389 sigma-api +``` -## Dépendances +## Mode développement / staging / production -Une dépendance, c'est un librairie JavaScript dont dépend le code source, soit pour le faire tourner soit pour faire tourner les outils dévs. Les dépendances développeur servent à tester par exemple. On trouve la liste des dépendances dans [`package.json`](./package.json). Express est un exemple de dépendance normale, nodemon et ESLint (voir infra) sont des dépendances dev (`devDependencies`). +TODO +Ca a un rapport avec NODE_ENV ? -Les dépendances s'installent avec `npm install`. Par défaut, toutes les dépendances sont installées. Si la variable `NODE_ENV` est configurée (vérifier avec la commande `echo "$NODE_ENV"`), +## Dépendances *npm* -* la valeur `development` installe tout -* la valeur `production` n'installe pas les dépendances développeur +On utilise un serveur Node.js avec [express.js](https://expressjs.com/) ; il est configuré dans [`app.ts`](../src/app.ts) puis lancé sur le port 3000 dans [`index.ts`](../src/index.ts). +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. -Certaines d'entre elles comme KnexJS ou Webpack doivent être installées globalement : +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 les plus importantes sont knex.js, notre outil de gestion de BDD locale, GraphQL qui sert à communiquer entre serveurs, ldap.js qui interroge le LDAP avec la plupart des données, webpack qui sert à la gestion du serveur. ESlint est un outil de vérification syntaxique. - -Le serveur utilisé est [express.js](https://expressjs.com/) ; il est configuré dans [`app.ts`](../src/app.ts) puis lancé sur le port 3000 dans [`index.ts`](../src/index.ts). +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. ## Configuration @@ -82,7 +147,7 @@ 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 +### Variables d'environnement | **Variable** | **Description** | **Défaut** (`ldap_config.json`) | | ------ | ------ | ----- | @@ -112,22 +177,28 @@ On peut définir ces variables d'environnement, **dans l'ordre décroissant de p ## Panneau d'administration -Il est accessible au path `/adminview/admin` ; n'importe quel path devrait rediriger dessus, ou alors vers `/adminview/login`. Les identifiants à utiliser sont ceux de Frankiz. L'authentification se fait par le LDAP Frankiz. +Il est accessible par navigateur au path `/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 -### Accès direct à la BDD via knex +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`. -Le panneau d'administration sert (ou plutôt, servira à terme) à accéder directement à la BDD propre de sigma. 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`. -Ces pages sont protégées pour n'être accessibles qu'en étant authentifié. +### GraphQL Voyager -### GraphQL Playground et Voyager +L'application Voyager, accessible à `/adminview/voyager`, permet de visualiser le « graphe » sous-jacent à la structure de l'API. -Accéder via un navigateur à `/graphql` et `/voyager` respectivement renvoie les apps GraphQL Playground et GraphQL Voyager. +## 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 donc soumises au mêmes permissions que dans l'API. +Accéder via un navigateur à `/graphql` renvoie l'application GraphQL Playground. -L'app Voyager permet de visualiser le « graphe » sous-jacent à la structure de l'API. Cet _endpoint_ devrait être protégé **en prod**. +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. Comme tout GraphQL Playground est géré directement par la package graphql, les requêtes dans le Playground sont soumises au mêmes permissions que dans l'API GraphQL [^doute]. GraphQL Playground est désactivé en production. -**En production**, +[^doute]: euuuuh à vérifier... ## Scripts diff --git a/src/adminview/admin_router.ts b/src/adminview/admin_router.ts index cf0f44cc0122c42ab41aef93b8e67934a0357f92..388e943ffa79d1093b996470503debe5d87b8191 100644 --- a/src/adminview/admin_router.ts +++ b/src/adminview/admin_router.ts @@ -20,7 +20,6 @@ * * @author manifold, kadabra * - * @todo whitelist the authorized admin uids; don't only use ensureLoggedIn * @todo check that REST API is finished and functional * @todo Les res.redirect() sont censes supporter les paths relatifs (et donc pas besoin de repreciser /adminview/* a chaque fois) * mais ca marche visiblement pas... Donc j'ai mis les paths absolus dans les res.redirect(). C'est un peu moche... essayer de modifier ca @@ -40,6 +39,28 @@ const whitelist = [ "lippou.tou", "guillaume.wang", ]; +/** + * @function ensureIsAdmin + * @summary Définit un middleware garantissant que la requête vient bien d'un utilisateur admin authentifié, et redirigeant vers returnTo sinon + * @argument String returnTo + */ +function ensureIsAdmin(returnTo) { + return (req, res, next) => { + // ensure that the request was authenticated by passport + ensureLoggedIn(returnTo); + // lookup req.user against whitelist of admin users + if (req.user.uid in whitelist) { + console.log("is an admin"); + } else { + console.log("is NOT an admin"); + req.flash('error: not an admin'); + req.logout(); + res.redirect(returnTo); + } + // go on + next(); + }; +} /** * @desc Création du Express router et setup de middlewares basiques @@ -52,6 +73,8 @@ const whitelist = [ */ const router = Router(); +//from https://www.npmjs.com/package/connect-flash: +//"The flash is a special area of the session used for storing messages. Messages are written to the flash and cleared after being displayed to the user." router.use(flash()); let port = process.env.PORT || 3000; @@ -63,11 +86,11 @@ let port = process.env.PORT || 3000; * /login ou sur /admin selon que l'utilisateur est connecté ou non. */ -router.get('/', function (req, res) { +router.get('/', (req, res) => { res.redirect('/adminview/admin'); }); -router.get('/avlogin', function (req, res) { +router.get('/avlogin', (req, res) => { // lets pug render adminview/views/login.pug with specified attributes res.render('login', { title: 'Login', port: port, @@ -76,16 +99,22 @@ router.get('/avlogin', function (req, res) { }); router.get('/admin', - ensureLoggedIn('/adminview/avlogin'), - function (req, res) { - // Une erreur a ce stade peut etre triggered si req.user n'existe pas - // mais pour autant on est assures que la personne est bien authentifiee - // donc on laisse passer sans déclencher d'erreur 500 - // (je suis à peu près sûr que ça ne peut jamais arriver ! pour moi si ça arrive c'est ultra bizarre et on ferait mieux de drop la requête. kadabra) + ensureIsAdmin('/adminview/avlogin'), + (req, res, next) => { let userName; if (req.user) { userName = req.user.uid; } else { + // Une erreur a ce stade peut etre triggered si req.user n'existe pas + // mais pour autant on est assures que la personne est bien authentifiee + // donc on laisse passer sans déclencher d'erreur 500 + // (je suis à peu près sûr que ça ne peut jamais arriver ! pour moi si ça arrive c'est ultra bizarre et on ferait mieux de déclencher une erreur ! à tester. --kadabra) + + /* + let err = new Error('Not authorized'); + res.status(403); + next(err); + */ console.log("Warning: une requête est arrivée à /adminview/admin et req.user n'existe pas"); userName = "personne"; } @@ -102,11 +131,11 @@ router.post('/avlogin', failureFlash: true }), (req, res) => { - // If this function gets called, authentication was successful. - // `req.user` contains the authenticated user. - // lookup req.user against whitelist of admin users + // If this function gets called, authentication was successful. + // `req.user` contains the authenticated user. console.log("POST request to /adminview/avlogin: OK, authenticated by LDAP. req.user: "); console.log(req.user); + // lookup req.user against whitelist of admin users if (req.user.uid in whitelist) { console.log("is an admin"); res.redirect("/adminview/admin"); @@ -119,7 +148,7 @@ router.post('/avlogin', } ); -router.post('/avlogout', function (req, res) { +router.post('/avlogout', (req, res) => { req.logout(); res.redirect('/adminview/admin'); }); @@ -130,7 +159,7 @@ router.post('/avlogout', function (req, res) { * affiche une représentation sous forme de graphe du schema GraphQL */ router.use('/voyager', - ensureLoggedIn('/login'), + ensureIsAdmin('/adminview/avlogin'), graphqlVoyager({ endpointUrl: '/graphql' }) ); @@ -138,55 +167,63 @@ router.use('/voyager', /** * @desc API REST fait-maison * ========================== - * `/admin/db/:table?columns=bla` fait une requete SELECT bla FROM table, et renvoie un JSON contenant le resultat + * GET `/adminview/db/table_name` renvoie une vue de la table table_name + * + * GET `/adminview/db/table_name?columns=bla` fait une requete + * SELECT bla FROM table_name + * et renvoie un JSON contenant le resultat + * + * TODO: à tester et rassifier + * TODO: je pense qu'on ferait mieux d'utiliser ca plutôt que router.get + * https://expressjs.com/en/4x/api.html#router.route */ -router.use('/voyager', - ensureLoggedIn('/login'), - graphqlVoyager({ endpointUrl: '/graphql' }) -); -// Je pense qu'on ferait mieux d'utiliser ca -// https://expressjs.com/en/4x/api.html#router.route -router.get('/db?', function (req, res) { - let table_name = req.query.table; - let columns = req.query.columns; +router.get('/db?', + ensureIsAdmin('/adminview/avlogin'), + (req, res) => { - res.redirect(`/adminview/db/${table_name}?columns=${columns}`); -}); + let table_name = req.query.table; + let columns = req.query.columns; + + res.redirect(`/adminview/db/${table_name}?columns=${columns}`); + }); /** * @function Knex_API: Get table * @summary Effectue une requête pour une table dans la BDD * @argument {string} table_name - La table voulue par l'utilisateur. */ -router.get('/db/:table_name?', function (req, res) { +router.get('/db/:table_name?', + ensureIsAdmin('/adminview/avlogin'), + (req, res, next) => { // get columns from query - let columns; - if (req.query.columns) { - columns = req.query.columns.split(','); - } else { - columns = null; - } - console.log(columns); - - knex.select(columns).from(req.params.table_name).then(function (table) { - res.setHeader("Content-Type", "application/json"); - res.write(JSON.stringify(table, null, 2)); - res.end(); - }, function () { - res.status(400); - res.render('error', { - status: res.statusCode, - error_message: "Bad request: can't find table " + req.params.table_name - }); - res.end(); - } - ); -}); + let columns; + if (req.query.columns) { + columns = req.query.columns.split(','); + } else { + columns = null; + } + console.log(columns); + + knex.select(columns).from(req.params.table_name).then( + function (table) { + res.setHeader("Content-Type", "application/json"); + res.write(JSON.stringify(table, null, 2)); + res.end(); + }, + // the following can only be executed if the previous one failed, since previous one calls res.end() + function () { + let err = new Error("Bad request: cannot find table " + req.params.table_name); + res.status(400); + next(err); + } + ); + }); /** * @desc Error handling * ==================== + * https://expressjs.com/en/guide/error-handling.html */ /** * @function Error_404_catcher @@ -199,19 +236,17 @@ router.use((req, res, next) => { }); /** - * @function Error_404_handler - * @summary Gère les erreurs 404 + * @function Error_handler + * @summary Gère les erreurs */ router.use((err, req, res, next) => { console.log("adminview: Entering error handler"); - res.locals.message = err.message; console.log(err.message); - res.status(err.status || 500); - let error_message = res.statusCode == 404 ? 'Not found.' : 'Internal server error.'; + //res.status(err.status || 500); res.render('error', { status: res.statusCode, - error_message: error_message + error_message: err.message }); });