Skip to content
Snippets Groups Projects
Forked from an inaccessible project.
admin_router.ts 9.05 KiB
/**
 * @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 = [
    "gregoire.grzeckowicz",
    "anatole.romon",
    "hadrien.renaud",
    "wilson.jallet",
    "quentin.chevalier",
    "guillaume.wang",
    "oliver.facklam",
    "octave.hazard",
    "guilhem.roy",
    "elia.azar"
];
/**
 * @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;