/** * @file Ce fichier définit le routage d'URL au sein de l'interface du _backend_. * Les URLs à résoudre ici sont les /adminview/* (et ce sont les seules). * * @desc Cette interface est destinée à n'être accessible qu'aux administrateurs du backend * Filtrage basique : déjà il faut que la personne connaisse l'adresse IP ou le hostname du backend... * L'authentification est basée sur passport avec ldapauth (comme pour les requêtes GraphQL), et * l'autorisation se fait à la main en vérifiant l'appartenance de l'uid à une whitelist. * * 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. * accessible par les paths `/adminview/db/:table?` * - à GraphQL Voyager, un package qui permet d'afficher une représentation sous forme de graphe du schéma GraphQL, * acessible par le path `/adminview/voyager` * - à 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`, l'interface admin ne fait que fournir un lien vers ce path * il est désactivé en production : "When NODE_ENV is set to production, GraphQL Playground (as well as introspection) is disabled as a production best-practice" * https://www.apollographql.com/docs/apollo-server/features/graphql-playground.html#Configuring-Playground * * @author manifold, kadabra * * @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 */ import { Router } from 'express'; // packages pour l'authentification import passport from 'passport'; import { ensureLoggedIn } from 'connect-ensure-login'; import flash from 'connect-flash'; // packages pour l'API REST et pour GraphQL Voyager import knex from '../../db/knex_router'; import { express as graphqlVoyager } from 'graphql-voyager/middleware'; // loads environment variables from (hidden) .env file import dotenv from 'dotenv'; dotenv.config(); let port = process.env.PORT; const whitelist = process.env.ADMINS.split(' '); /** * @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 && whitelist.includes(req.user.uid)) { console.log("is an admin"); // go on next(); } 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 * ================================================================= * router: an Express router. https://expressjs.com/en/4x/api.html#router * See also https://scotch.io/tutorials/learn-to-use-the-new-router-in-expressjs-4 * = a "sub-middleware stack", a “mini-application" inside the main application * * router automatically inherits from views and 'pug' view engine, that were set in app.ts (app.set(...)) */ 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()); /** * @desc Paths pour l'authentification : login, logout, page d'accueil * =================================================================== * Le login se fait en POST. Faire un GET à la racine / renvoie sur * /login ou sur /admin selon que l'utilisateur est connecté ou non. */ router.get('/', (req, res) => { res.redirect('/adminview/admin'); }); router.get('/avlogin', (req, res) => { // lets pug render adminview/views/login.pug with specified attributes res.render('login', { title: 'Login', port: port, errorMessage: req.flash('error') }); }); router.get('/admin', 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"; } res.render('home', { title: 'Home', port: port, userName: userName }); } ); router.post('/avlogin', passport.authenticate('ldapauth', { failureRedirect: '/adminview/avlogin', failureFlash: true }), (req, res) => { // 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); // redirect to /admin // in /admin, user will be looked up against whitelist anyway res.redirect('/adminview/admin'); } ); router.post('/avlogout', (req, res) => { req.logout(); res.redirect('/adminview/admin'); }); /** * @desc GraphQL Voyager * ===================== * affiche une représentation sous forme de graphe du schema GraphQL */ router.use('/voyager', ensureIsAdmin('/adminview/avlogin'), graphqlVoyager({ endpointUrl: '/graphql' }) ); /** * @desc API REST fait-maison * ========================== * 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.get('/db?', ensureIsAdmin('/adminview/avlogin'), (req, res) => { 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?', 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(req.params.table_name); 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 * @summary Catche les requêtes en dehors des URL acceptées */ router.use((req, res, next) => { let err = new Error('Not found'); res.status(404); next(err); }); /** * @function Error_handler * @summary Gère les erreurs */ router.use((err, req, res, next) => { console.log("adminview: Entering error handler"); console.log(err); //console.log(err.message); //res.status(err.status || 500); res.render('error', { status: res.statusCode, error_message: err.message }); }); export default router;