Skip to content
Snippets Groups Projects
Forked from an inaccessible project.
app.ts 10.59 KiB
/** 
 * @file Initialise et configure le serveur Express sur lequel tourne le back.
 * 
 * Inclut les middlewares des packages utilisés: apollo-server, passportjs, morgan...
 * et branche nos propres middleware : l'API GraphQL et l'interface admin du backend (adminview).
 * Pour comprendre ce que fait chaque package, se référer à leur page sur https://www.npmjs.com.
 * 
 * On peut considérer que les app.use (et app.get et app.post) sont pattern-matchés et
 * exécutés séquentiellement. http://expressjs.com/en/guide/using-middleware.html
 * 
 * @author manifold, kadabra
 */
import express from 'express';
import bodyParser from 'body-parser';
// packages pour graphql
import { express as graphqlVoyager } from 'graphql-voyager/middleware';
import { ApolloServer } from 'apollo-server-express';
import schema from './graphql/schema'; // definition du schéma et des resolvers
// l'interface admin du backend (adminview)
import router from './adminview/admin.router';
// packages pour l'authentification
import passport from 'passport';
import expressSession from 'express-session';
import cookieParser from 'cookie-parser';
import cors from 'cors';
// HTTP request logger
import morgan from 'morgan';
// packages pour pouvoir importer depuis des fichiers de config
import path from 'path';

// config des paramètres de connexion au LDAP
import { ldapConfig, credentialsLdapConfig } from './ldap/config';
const { dn, passwd } = credentialsLdapConfig;

// configure passport, pour l'authentification ldap et pour comment gérer les sessions (serializeUser/deserializeUser)
import './config_passport';

/**
 * @desc Création de l'application Express et setup de middlewares basiques
 * ========================================================================
 */
const app = express();
// parse incoming HTTP request bodies, available under the req.body property
app.use(bodyParser.json()); //parses bodies of media type "application/json"
app.use(bodyParser.urlencoded({ //parses bodies of media type "application/x-www-form-urlencoded"
    extended: true //use qs library (see https://www.npmjs.com/package/body-parser#bodyparserurlencodedoptions)
}));
// parse Cookie header and populate req.cookies with an object keyed by the cookie names
//app.use(cookieParser()); <-- req.cookies n'est pas utilisé.
// ne *pas* inclure de header HTTP publicisant que l'application tourne sous Express
app.disable('x-powered-by');
// setup morgan (HTTP request logger middleware)
app.use(morgan('dev'));

const FRONTEND_SERVER_URL = process.env.FRONTEND_SERVER_URL || 'http://localhost:8888';
// Options de configuration pour le _middleware_ `cors`.
// CORS = Cross Origin Resource Sharing
const corsOptions = {
    //configures the "Access-Control-Allow-Origin" CORS header, declaring that sigma-back wants to make resources accessible to that site (and that site only)
    origin: FRONTEND_SERVER_URL, 
    //configures the "Access-Control-Allow-Credentials" CORS header, allowing cookies to be included on cross-origin requests
    credentials: true 
};
app.use(cors(corsOptions));

/**
 * @desc Authentification de la requête (cookie)
 * =============================================
 * Si la requête possède un cookie, on regarde dans la session s'il s'agit d'un cookie valide.
 * - Si oui, on récupère l'uid de l'utilisateur correspondant dans la session.
 * - Si non, on passe quand même un uid spécial signifiant que l'utilisateur n'est pas authentifié.
 * L'uid qu'on a trouvé est stocké dans req.user, et sera passé à GraphQL, dans le context.
 * 
 * NB : la session ne contient que les correspondances cookie / uid, et rien d'autre.
 * passport.serializeUser et .deserializeUser servent en théorie à traduire 
 * 
 * https://stackoverflow.com/questions/22052258/what-does-passport-session-middleware-do
 * 
 */

 /**
 * it is also here that we define parameters for *session store*. https://redislabs.com/blog/cache-vs-session-store/
 * configure this right, as express-session docs warns that the default config is not good for prod!!
 * @todo [critical] configure express-session (session store and other options)
 * @todo choose a session secret and where to store it
 * https://www.npmjs.com/package/express-session
 */
// load data from the session identified by the cookie (if one exists), into req.session
// on ne manipulera pas req.session directement, on laisser toujours passport le faire pour nous
 app.use(expressSession({
    //secret: ldapConfig.sessionSecret, <-- wtf? nothing to do with ldap!
    secret: "asdfjklkjfdsasdfjklkljfdsa",

}));
app.use(passport.initialize());
//passport.session(): load the user object onto req.user if a serialised user object was found in the req.session
// aucun effet sur les requetes sans cookie ou les requetes avec cookie expired,
// puisqu'elles ne sont pas dans req.session, puisque non-reconnues par app.use(expressSession(...))
//(see http://toon.io/understanding-passportjs-authentication-flow/)
app.use(passport.session(), (req, res, next) => {
    const user = req.user ? req.user.uid : "none";
    console.log(`passport.session: found user: ${user}, authenticated: ${req.isAuthenticated()}`);
    next();
}); 

/**
 * @desc Répondre aux requêtes de connexion par LDAP (venant du front)
 * ===================================================================
 * i.e. endpoint for frontend's authentication requests (logins through adminview/avlogin are caught by the router in admin_router.js)
 * i.e. quand l'utilisateur submit le formulaire de login avec ses identifiants/mdp dans le front
 * 
 * @todo gérer le cas où une requête à /login est reçue, mais où cette requête contient un cookie valide
 */

//with custom callback:
//http://www.passportjs.org/docs/authenticate/#custom-callback
// http://toon.io/understanding-passportjs-authentication-flow/
app.post('/login', (req, res, next) => {
    console.log("Received an authentication request to /login");
    passport.authenticate('ldapauth', (err, user, info) => {
        console.log("| Entering passport.authenticate('ldapauth', - ) callback");
        // If an exception occurred
        if (err) {
            console.log("| Error when trying to passport.authenticate with ldapauth");
            console.log(err);
            return res.status(err.status).json({
                message: "Exception raised in backend process during authentication: " + err,
                authSucceeded: false
            });
            // return next(err); // handle error? or drop request and answer with res.json()?
        }
        // If authentication failed, user will be set to false
        if (!user) {
            console.log("| Authentication failed, passport.authenticate did not return a user. ");
            return res.status(401).json({
                message: "Authentication failed: " + info.message,
                authSucceeded: false
            });
        }
        req.login(user, (err) => {
            // If an exception occurred at login
            if (err) {
                console.log("| Error when trying to req.login in callback in passport.authenticate('ldapauth', - )");
                console.log(err);
                return res.status(err.status).json({
                    message: "Exception raised in backend process during login: " + err,
                    authSucceeded: false
                });
                // return next(err); // handle error? or drop request and answer with res.json()?
            }
            // If all went well
            console.log("| Authentication succeeded! :)");
            // passport.authenticate automatically includes a Set-Cookie HTTP header in 
            // the response. The JSON body is just to signal the frontend that all went well
            return res.status(200).json({
                message: 'Authentication succeeded',
                authSucceeded: true
            });
        });
    })(req, res, next);
});

/**
 * @desc Servir l'API GraphQL à proprement parler
 * ==============================================
 */

// Define GraphQL request's context object, through a callback, with authorization.
// See: https://github.com/apollographql/apollo-server/blob/master/docs/source/best-practices/authentication.md

const context = async ({ req }) => {
    let uid;
    let password;
    
    console.log("Responding to graphql request...");
    console.log(`
    | User: ${req.user ? req.user.uid : "none"}
    | Authorization: ${req.headers.authorization}
    | Authenticated: ${req.isAuthenticated()}
    `.trim());
    
    if(req.isAuthenticated()) {
        console.log("graphql API is receiving a request from an authenticated user! \\o/");
        try {
            uid = req.user.uid;
            password = "mythe";
        } catch (err) {
            console.log("Error: req is authenticated, but pb when trying to extract uid from req.user. Probably user was either not serialized or not deserialized properly");
            console.log(err);
        }
    } else {
        // FOR DEVELOPMENT ONLY. for production, replace with a "publicUser" or "notLoggedInUser" or something.
        uid = dn.split("=")[1].split(",")[0];
        password = passwd;
    }
    
    return {
        request: req,
        user: { uid, password }
    }
}

const server = new ApolloServer({
    ...schema,
    context,
    playground: {
        settings: {
            "editor.theme": "dark",
            "editor.cursorShape": 'line'
        }
    }
});

server.applyMiddleware({ app });

/**
 * @desc Servir l'interface admin du backend ("adminview")
 * =======================================================
 * On redirige vers la "mini-application" adminview. 
 * Il s'agit en fait d'un express.Router, qui sert a creer un "sous-middleware stack".
 * Par cette interface admin, on a accès :
 * - à une API REST fait-maison (peut-être encore à débugger)
 * - à GraphQL Voyager, un package qui permet d'afficher une représentation sous forme de graphe du schéma GraphQL.
 */
app.use('/adminview', router); // catches and resolves HTTP requests to paths '/adminview/*'

/**
 * @desc Catch-all
 * ===============
 * Normalement aucune requête ne devrait arriver au dernier middleware :
 * les requêtes GraphQL doivent être adressées à /graphql,
 * les requêtes de connexion du frontend doivent être adressées à /login,
 * les requêtes vers l'adminview doivent être adressées à /adminview/*
 * Donc c'est qu'il y a un problème quelque part. On le log et on répond 500 Internal Server Error (ou 400 Bad Request?).
 * 
 * @todo faire un loggage plus propre que ça...
 */
app.use('/', (req, res) => {
    console.log("Erreur: une requête est arrivée au dernier middleware");
    res.status(500).send({ error: "unexpectedly triggered catchall" });
});

export default app;