Skip to content
Snippets Groups Projects
Commit 627e63b4 authored by Wilson JALLET's avatar Wilson JALLET :money_with_wings:
Browse files

Merge

parents 4ec0ae04 8934e632
No related branches found
No related tags found
No related merge requests found
......@@ -10,7 +10,7 @@ Pour obtenir une copie de ce dépôt, clonez-le avec
ou `git clone https://gitlab.binets.fr/br/sigma-backend.git`, puis installez les dépendences JavaScript avec `npm install`.
A terme, ce projet doit tourné sur un serveur de l'école polytechnique et fournir à un serveur front *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é...).
A terme, ce projet doit tourner sur un serveur de l'Ecole polytechnique et fournir à un serveur frontend *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 indifferemment serveur front, front ou frontend...)
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.
......@@ -115,15 +115,22 @@ Pour accéder à la "vraie" BDD, sur roued (le serveur qui héberge sigma), il f
* se connecter à la BDD via la commande `psql`
* faire les requetes en SQL par l'interface de postgreSQL.
## API/panneau d'administration
## Panneau d'administration
### Authentification
L'authentification se fait contre le LDAP en envoyant un requête HTTP POST à '/login'. En fonction de la valeur du header `Accept` inclus dans la requête, on a deux comportements possibles
L'authentification se fait contre le LDAP en envoyant un requête HTTP POST à '/adminview/avlogin'. C'est une page distincte de '/login' qui est utilisé pour les requetes d'authentification à partir du client front, pour pouvoir facilement distinguer les deux comportements :
- login depuis le panneau d'administration (POST à '/adminview/avlogin') : `passport.authenticate(...)` puis redirection vers GET '/adminview/admin'
- login depuis le client front (requête POST à '/login']) : `passport.authenticate(...)` puis renvoyer une reponse au client
| `application/json` | autre |
| ------------------------------------ | ---------------------- |
| Renvoie un message de succès/d'échec | Redirige vers `/admin` |
### 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. 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 authentifé.
### GraphiQL et Voyager
A partir du panneau d'admin, en faisant des requêtes GET à '/graphiql' et '/voyager' respectivement, on accède à GraphiQL et à GraphQL Voyager. Ces pages sont protégées pour n'être accessibles qu'en étant authentifé.
## Scripts
......
......@@ -5,20 +5,20 @@ import path from 'path';
/**
* @description Configuration de l'authentification
* @author guillaume.wang
* @author kadabra
*
* on a besoin d'authentification pour 2 trucs :
* - l'acces a l'interface admin (admin_view) du back, definie dans admin_router.js
* - le contexte graphQL
*
* serializeUser et deserializeUser: passport s'attend a ce qu'on ait besoin d'avoir req.user disponible partout dans notre code
* En gros l'idee de passport c'est: serializeUser permet d'obtenir une cle identifiant chaque user
* et deserializeUser prend cette cle, fait une requete vers une BDD de users et met dans l'objet JS req.user toutes les infos issues de la BDD
* serializeUser permet d'obtenir une cle identifiant chaque user
* deserializeUser fait une requete vers une BDD de users en utilisant cette cle, et met dans l'objet JS req.user toutes les infos issues de la BDD
* Cette repartition permet de ne stocker dans la session (i.e. en memoire sur le serveur) que la cle des utilisateurs connectes et de ne "charger en memoire" toutes les infos de la BDD que lorsque necessaire
* cf https://stackoverflow.com/questions/27637609/understanding-passport-serialize-deserialize#27637668
* et http://toon.io/understanding-passportjs-authentication-flow/
*
* Mais en fait dans notre cas c'est graphql qui communique avec la BDD, donc on s'en fiche! On peut se contenter de dire a serializeUser et deserializeUser de ne s'occuper que du champ uid)
* (on pourrait penser que passer par deserializeUser permettrait de reduire le nombre d'interactions avec la BDD, mais en fait non car deserializeUser est appele *a chaque requete*)
*/
const configPath = path.resolve('./', 'ldap_config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
......@@ -35,10 +35,12 @@ passport.use(new LdapStrategy({
},
//usernameField: 'username', // Field name where the username is found, defaults to username
//passwordField: 'password', // Field name where the pas sword is found, defaults to password
//passwordField: 'password', // Field name where the password is found, defaults to password
// LdapStrategy has a default verify callback ! j'ai perdu plein de temps pour rien :'(
// cf. https://github.com/vesse/passport-ldapauth/blob/master/lib/passport-ldapauth/strategy.js, line 195 (` var verify = function() { ... } `)
// given how LdapStrategy is coded, it is not necessary to do a verify callback
// https://github.com/vesse/passport-ldapauth/blob/master/lib/passport-ldapauth/strategy.js#L230
// (note that LdapStrategy has no default verify callback, the "verify" function (L105) is actually the "done" function that is called by the verify callback if we choose to make one)
// we leave this commented out as a template for future use
/*
function (user, done) {
// "verify callback", called after each passport.authenticate(...),
......@@ -61,12 +63,13 @@ passport.use(new LdapStrategy({
//toujours bon a savoir pour faire des tests:
//The result of the serializeUser method is attached to the session as req.session.passport.user
passport.serializeUser(function (user, done) {
console.log(`serializing user ${user.uid}`); // DEBUG
console.log(`passport.serializeUser(): serializing user ${user.uid}`); // DEBUG
done(null, user.uid);
});
//The first argument of deserializeUser corresponds to the key of the user object that was given to the done function in serializeUser
//The fetched object is attached to the request object as req.user (available in all subsequent middleware)
passport.deserializeUser(function (userUid, done) {
console.log(`passport.deserializeUser(): deserializing user ${userUid}`); // DEBUG
done(null, { uid: userUid });
});
\ No newline at end of file
......@@ -2,16 +2,20 @@
* @file Lance le serveur configuré dans {@link server.js} en y ajoutant le routeur d'URL.
* @author manifold
*/
import server from './server';
import app from './server';
import colors from 'colors';
import router from './routing/admin_router';
import passport from 'passport';
// setting up l'interface admin des BDD
server.use('/',router); // catches and resolves HTTP requests to paths '/*'
app.use('/adminview',router); // catches and resolves HTTP requests to paths '/adminview/*'
app.get('/', // catches all other GET requests
((req, res, next) => res.redirect('/adminview'))
);
let port = process.env.PORT || 3000;
server.listen(port, () => {
app.listen(port, () => {
console.log(colors.blue(`Express server listening on port ${port}`));
});
......@@ -4,7 +4,7 @@
* permettant de consulter la base de donnée interne à Sigma, via des requêtes construites avec Knex.
* @author manifold
*
* Les res.redirect() sont censes supporter les paths relatifs (et donc pas besoin de repreciser /* a chaque fois)
* 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().
*/
import express from 'express';
......@@ -23,20 +23,22 @@ let port = process.env.PORT || 3000;
*/
router.get('/', function (req, res) {
console.log("GET handler for / route");
console.log("GET handler for /adminview route");
console.log('Connecting to ' + req.url);
console.log('Trying to go to admin page...');
res.redirect('/admin');
res.redirect('/adminview/admin');
});
router.get('/login', function (req, res) {
router.get('/avlogin', function (req, res) {
console.log('Connecting to ' + req.url);
res.render('login', { title: 'Login', port: port,
errorMessage: req.flash('error') }); //lets pug render src/views/login.pug with specified attributes
res.render('login', {
title: 'Login', port: port,
errorMessage: req.flash('error')
}); //lets pug render src/views/login.pug with specified attributes
});
router.get('/admin',
ensureLoggedIn('/login'),
ensureLoggedIn('/adminview/avlogin'),
function (req, res) {
console.log('Connecting to ' + req.url);
let userName;
......@@ -46,7 +48,7 @@ router.get('/admin',
try {
let user = req.user;
//let user = req.user;
console.log('Welcome,',user.uid);
console.log('Welcome,', user.uid);
userName = user.uid;
} catch (err) {
console.log("Warning: in admin_router router.get('/admin')");
......@@ -57,9 +59,34 @@ router.get('/admin',
}
);
router.post('/logout', function (req, res) {
router.post('/avlogin',
passport.authenticate('ldapauth', {
successRedirect: '/adminview/admin',
failureRedirect: '/adminview/avlogin',
failureFlash: true
}
// on a besoin de faire un callback apres le passport.authenticate car
// on souhaite garde l'information user.dn et body.password qq part.
// TODO: essayer de garder ces informations plus proprement...
// EDIT: en fait apparemment on a pas besoin de ces informations du tout
/*
function (req, res) {
req.session.dn = req.user.dn;
req.session.password = req.body.password;
if (req.session.returnTo !== undefined) {
res.redirect(req.session.returnTo); //TODO: <- euh ok ca marche mais c'est quoi ca?
} else {
res.redirect("/admin");
}
}
*/
)
);
router.post('/avlogout', function (req, res) {
req.logout();
res.redirect('/');
res.redirect('/adminview');
});
// je pense qu'on ferait mieux d'utiliser ca
......@@ -68,7 +95,7 @@ router.get('/db?', function (req, res) {
let table_name = req.query.table;
let columns = req.query.columns;
res.redirect(`/db/${table_name}?columns=${columns}`);
res.redirect(`/adminview/db/${table_name}?columns=${columns}`);
});
/**
......@@ -102,30 +129,6 @@ router.get('/db/:table_name?', function (req, res) {
);
});
router.post('/login', (req, res, next) => {
passport.authenticate('ldapauth', (err, user, info) => {
if (err) return next(err); // handle error
else if (!user) {
return res.json(403, {message: "Not authenticated."});
}
req.login(user, (err) => {
if (err) {
console.log(err);
return err;
}
// if request asks for a json, reply with a token
// else redirect to admin panel
(req.header('accept') == 'application/json')
? res.json({
message: 'Authentication succeeded.'
})
: res.redirect('/admin');
});
})(req,res,next);
});
/**
* @function Error 404 catcher
......
/**
* @file Ce fichier définit le routage d'URL au sein de l'interface du _backend_.
* Il définit la page de connexion `/`, le panneau administrateur `/admin` et l'API REST \(`/db/:table?`)
* permettant de consulter la base de donnée interne à Sigma, via des requêtes construites avec Knex.
* @author manifold
*
* 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().
*/
import express from 'express';
import knex from '../../db/knex_router';
import passport from 'passport';
import { ensureLoggedIn } from 'connect-ensure-login';
const router = express.Router();
let port = process.env.PORT || 3000;
/**
* @description 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('/', function (req, res) {
console.log("GET handler for /adminview route");
console.log('Connecting to ' + req.url);
console.log('Trying to go to admin page...');
res.redirect('/adminview/admin');
});
router.get('/avlogin', function (req, res) {
console.log('Connecting to ' + req.url);
res.render('login', { title: 'Login', port: port,
errorMessage: req.flash('error') }); //lets pug render src/views/login.pug with specified attributes
});
router.get('/admin',
ensureLoggedIn('/adminview/avlogin'),
function (req, res) {
console.log('Connecting to ' + req.url);
let userName;
// 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
try {
let user = req.user;
//let user = req.user;
console.log('Welcome,',user.uid);
userName = user.uid;
} catch (err) {
console.log("Warning: in admin_router router.get('/admin')");
console.log(err.message);
userName = "No one";
}
res.render('home', { title: 'Home', port: port, userName: userName });
}
);
router.post('/avlogin',
passport.authenticate('ldapauth', {
successRedirect: '/adminview/admin',
failureRedirect: '/adminview/avlogin',
failureFlash: true
}
// on a besoin de faire un callback apres le passport.authenticate car
// on souhaite garde l'information user.dn et body.password qq part.
// TODO: essayer de garder ces informations plus proprement...
// EDIT: en fait apparemment on a pas besoin de ces informations du tout
/*
function (req, res) {
req.session.dn = req.user.dn;
req.session.password = req.body.password;
if (req.session.returnTo !== undefined) {
res.redirect(req.session.returnTo); //TODO: <- euh ok ca marche mais c'est quoi ca?
} else {
res.redirect("/admin");
}
}
*/
)
);
router.post('/avlogout', function (req, res) {
req.logout();
res.redirect('/adminview');
});
// 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;
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) {
// 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();
}
);
});
/**
* @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');
err.status = 404;
next(err);
});
/**
* @function Error 404 handler
* @summary Gère les erreurs 404
*/
router.use((err, req, res, next) => {
console.log("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.render('error', {
status: res.statusCode,
error_message: error_message
});
});
export default router;
......@@ -7,13 +7,11 @@
*/
import express from 'express';
import cookieParser from 'cookie-parser';
import schema from './graphql/schema';
import { express as graphqlVoyager } from 'graphql-voyager/middleware';
import graphqlHTTP from 'express-graphql'; // new name of 'graphql-server-express'. cf npmjs.com
import flash from 'connect-flash';
import { ensureLoggedIn } from 'connect-ensure-login';
import flash from 'connect-flash'; // utilise pour admin_view
import passport from 'passport';
import './auth';
import session from 'express-session';
import bodyParser from 'body-parser';
import favicon from 'serve-favicon';
......@@ -22,48 +20,52 @@ import path from 'path';
import fs from 'fs';
import cors from 'cors';
const server = express();
import schema from './graphql/schema';
import './auth';
const app = express();
//The app object conventionally denotes the Express application
// Parse incoming HTTP request bodies, available under the req.body property
// cf www.npmjs.com/package/body-parser
server.use(bodyParser.json()); //parses bodies of media type "application/json"
server.use(bodyParser.urlencoded({ //parses bodies of media type "application/x-www-form-urlencoded"
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 (quoi que ca veuille dire o.O)
}));
server.use(cookieParser());
app.use(cookieParser());
const configPath = path.resolve('./', 'ldap_config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// Config de passport : le "import './auth';" plus haut execute la configuration de l'objet passport. cf, donc, auth.js
// Définit les paramètres de stockage des sessions.
server.use(session({
app.use(session({
secret: config.sessionSecret,
resave: true,
saveUninitialized: false
}));
server.use(passport.initialize());
server.use(passport.session());
app.use(passport.initialize());
app.use(passport.session());
/* fin de Configuration de l'authentification */
// cache le fait que l'application tourne sous Express dans le header HTTP.
server.disable('x-powered-by');
app.disable('x-powered-by');
// setting up view engine for pug
console.log("Running at",__dirname);
let viewpath = path.resolve(__dirname,'views');
server.set('views', viewpath);
server.set('view engine', 'pug');
app.set('views', viewpath);
app.set('view engine', 'pug');
// favicon: capital sigma symbol
server.use(favicon(path.resolve('./','assets','favicon.ico')));
app.use(favicon(path.resolve('./','assets','favicon.ico')));
// specifies path to static assets
server.use('/assets',express.static(path.resolve('./','assets')));
app.use('/assets',express.static(path.resolve('./','assets')));
// Morgan is middleware for logging requests
server.use(morgan('dev'));
app.use(morgan('dev'));
const defaultUser = require('./../ldap_connexion_config.json');
......@@ -73,20 +75,102 @@ const corsOptions = {
origin: 'http://localhost:8888', // Configures the Access-Control-Allow-Origin CORS header. i.e. specifies that sigma-back wants to make resources accessible to this site (and this site only)
credentials: true // Configures the Access-Control-Allow-Credentials CORS header. i.e. allows cookies to be included on cross-origin requests
};
server.use(cors(corsOptions));
app.use(cors(corsOptions));
const SECRET_KEY = "azojgc;aegpfrihzcksdlmpqsqkx";
/*
const addUser = async (req, res, next) => {
const token = req.cookies.csrftoken;
if (!token) return next();
console.log(`Token is ${token}`);
try {
const { user } = jwt.verify(token, SECRET_KEY);
req.user = user;
} catch (err) {
console.log('Cookie error',err);
}
};
app.use(addUser);
*/
//endpoint for frontend's authentication requests
//with custom callback:
//http://www.passportjs.org/docs/authenticate/#custom-callback
// http://toon.io/understanding-passportjs-authentication-flow/
app.post('/login', (req, res, next) => {
passport.authenticate('ldapauth', (err, user, info) => {
// If an exception occurred
if (err) {
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) {
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(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
res.json({
message: 'Authentication succeeded',
authSucceeded: true
});
});
})(req, res, next);
});
//without custom callback:
/*
// http://toon.io/understanding-passportjs-authentication-flow/
app.post('/login',
passport.authenticate('ldapauth'),
function (req, res) {
// If this function gets called, authentication was successful.
// `req.user` contains the authenticated user.
console.log("Frontend authentication succeeded");
res.json({
message: 'Authentication succeeded',
authSucceeded: true
});
}
);
*/
server.use('/graphql',
app.use('/graphql',
bodyParser.json(), // parse incoming HTTP request (req) as a JSON
graphqlHTTP(async (req, res, params) => {
// vary the options *on a per-request basis*
let uid;
let password;
console.log("Responding to graphql request...");
console.log(`User ${req.user ? req.user.uid : "none"}`);
console.log("User authenticated:",req.isAuthenticated());
console.log("User is authenticated:",req.isAuthenticated());
if(req.isAuthenticated()) {
try {
......@@ -113,13 +197,13 @@ server.use('/graphql',
);
// GraphQL voyager affiche une représentation sous forme de graphe du schema GraphQL
server.use('/voyager',
app.use('/voyager',
/*ensureLoggedIn('/login'),*/
graphqlVoyager({ endpointUrl: '/graphql' })
);
// connect-flash is middleware for flashing messages
// used in sigma-back's admin interface
server.use(flash());
app.use(flash());
export default server;
export default app;
......@@ -6,4 +6,4 @@ block extraStyles
block content
h1 Error #{status}
p #{error_message}
a(href="/admin") Go back
\ No newline at end of file
a(href="/adminview/admin") Go back
\ No newline at end of file
......@@ -5,7 +5,7 @@ block content
h2 Query the database
p Hello, world! This is server talking to you live on port #{port}!
p You can use the REST API to query the database using the form below.
form(action="/db", method="get")
form(action="/adminview/db", method="get")
div.form-group
label(for="table") Table
input.form-control(type="search", name="table")
......
......@@ -6,7 +6,7 @@ block content
p Veuillez vous connecter.
p <em>Please log in.</em>
p #{errorMessage}
form(action="/login", method="post")
form(action="/adminview/login", method="post")
div.form-group
label(for="username") Frankiz ID
input.form-control(type="text", placeholder="User", name="username")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment