TodoList Con Node.js Y MongoDB - Parte II

Lo prometido es deuda, aquí vamos con la segunda parte donde integraremos Passport para poder validar los request y contronlar sesiones de usuarios.

Primero debemos agregar los siguientes imports al archivo app.js y la estrategia (plugin de autenticación) a utilizar. Además se agregan los archivos UserModel.js y security.js extensión del modelo de User y métodos de validación del request respectivamente:

1
2
3
4
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var security = require('./config/security');
var User = require('./model/UserModel');

Ahora configuramos Express para que pueda utilizar Passport como administrador de sesiones de usuario. Es necesario que express use su propio administrador de sesiones y además debe ser inicializado antes que el de passport.

1
2
3
4
5
6
app.use(express.cookieParser('your secret here'));
app.use(express.session());

// passport initialize
app.use(passport.initialize());
app.use(passport.session());

Luego agregamos mecanismos de autenticación, serialización y deserialización a passport:

1
2
3
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

Model

Ahora extendemos el modelo de usuario para agregar nuestros atributos (si es es necesario, por ahora lo dejaremos vacío):

1
2
3
4
5
6
7
8
9
10
var mongoose = require('mongoose');
var passportLocalMongoose = require('passport-local-mongoose');

var UserSchema = new mongoose.Schema({});

UserSchema.plugin(passportLocalMongoose);

var UserModel = mongoose.model('User', UserSchema);

module.exports = UserModel;

Seguridad

Hay un archivo que se llama security.js que contiene una función que valida si el request solicitado está autenticado por passport, si está autenticado lo “deja pasar” (next), de lo contrario lo redirecciona al template de login.

1
2
3
4
5
6
exports.ensureAuthenticated = function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
};

Nos queda asegurar las urls o recursos que necesitemos con la función ensureAuthenticated en el archivo app.js. La idea es que sea lo menos intrusivo posible. Hay dos formas de hacerlo, la primera es intrusiva (y no recomendada aunque puede utilizarse con fines específicos) y se debe realizar por cada request en los archivos que contienen los Controllers:

1
2
3
4
5
6
7
exports.authenticationExample = function(req, res) {
if (req.isAuthenticated()) {
// Do something
} else {
res.redirect('/login');
}
}

La segunda es interceptando los request y validando según algún patrón, que es nuestro caso. Esta configuración esta en app.js donde se declaran las rutas a los Controllers:

1
2
app.all('/api/*', security.ensureAuthenticated);
app.all('/todos', security.ensureAuthenticated);

Con esto interceptamos todos los request que van a la API y los que van a /todos y los hacemos pasar por la función de autenticación de request. Con esto evitamos poner dichas líneas de autenticación en cada controller.

Login y Logout

Finalmente nos queda la última parte respecto de la autenticación y es configurar el login y el logout. Para el login usamos la API de passport y delegamos el trabajo de autenticación de la siguiente forma (configuración):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Funcion sin autenticación que permite desplegar el template de Login
*/
app.get('/login', function(req, res) {
res.render('login', {});
});

/**
* Función de login que permite delegar la autenticacion en la API de
* passport, se configura además cuando la autenticación es exitosa a que url
* lo debe redireccinar y lo mismo en caso de error.
*/
app.post('/login', passport.authenticate('local', {
successRedirect: '/todos',
failureRedirect: '/login'
}));

El logout es más sencillo y lo único que hay que hacer es invalidar el request, destruir la sesión del usuario y redireccionar al login.

1
2
3
4
app.get('/logout', function(req, res) {
req.logout();
res.redirect('/login');
});

Me queda pendiente en la próxima parte del tutorial explicar cómo consumir estos servicios usando angular y desplegar los templates.

TodoList con Node.js y MongoDB - Parte I

Llevo un tiempo estudiando y haciendo algunas cosas con Node.js y me he llevado una grata impresión con esta plataforma. Al principio cuesta un poco entender el cómo funciona ya que el paradigma es muy distinto del que he venido usando durante este último tiempo.

Me animé y empece a usar Node.js para hacer algo simple y así tratar de entender el cómo funciona. Para hacer algo mas sabrosa la experiencia agregué algunos ingredientes adicionales, un framework para desarrollo web llamado Express, Mongoose para modelar los documentos que van a ir a parar a MongoDB, Passport para autenticar los request (control de usuarios y permisos), uso de templates con EJS y finalmente para la vista usamos el framework AngularJS. Como podrán ver es un stack de tecnologías basadas en Javascript.

Para iniciar voy a explicar algunas partes del código que personalmente me costó entender e implementar. Algunos de esos puntos fueron la integración con passport para la autenticación de los request, control de usuario y uso de datos en sesión.

Configuración de node.js

Cuando utilizamos nodejs siempre hay un archivo principal donde se configura toda la plataforma, en este caso el archivo es llamado app.js. Voy a ir mostrando ciertas partes del código (si quieren ver el código, esta disponible en github):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');

var indexController = require('./routes/IndexController');
var todosController = require('./routes/TodosController');
var registerController = require('./routes/RegisterController');

var http = require('http');
var path = require('path');
var app = express();
var db = require('./config/database');


var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;

var security = require('./config/security');
var User = require('./model/UserModel');

Controllers

Lo primero es incluir el módulo de express y luego los Controllers de la aplicación. Veamos el código de IndexController

1
2
3
4
5
exports.index = function(req, res) {
res.render('todos', {
user: req.user
});
};

Este es el Controller mas simple de la aplicación, lo único que hace es exponer mediante exports la función index cuya responsabilidad es desplegar la página principal de la aplicación. Lo demás está en el controlador de la API que será llamada desde dicha página (usando ajax).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
var Todo = require('../model/TodosModel');

/**
* Busca todos los todos para el usuario registrado
* @param {Object} req request
* @param {Object} res response
*/
function findAllTodosByUser(req, res) {
var userId = req.user.id;

Todo.find()
.where('creator')
.equals(userId)
.sort('date')
.exec(function(err, todos) {
if (err) {
res.send(err);
}
res.json(todos);
});
}

exports.allTodos = findAllTodosByUser;

/**
* Crea un todo para el usuario
* @param {Object} req request
* @param {Object} res response
* @return {Object} Lista de todos
*/
exports.createTodo = function(req, res) {
var userId = req.user.id;
var textTodo = req.body.text;

Todo.create({
text: textTodo,
done: false,
creator: userId
}, function(error, todo) {
if (error) {
res.send(error);
}
findAllTodosByUser(req, res);
});
}

/**
* Elimina un Todo por su ID
* @param {Object} req request
* @param {Object} res response
* @return {Object} Lista de todos
*/
exports.deleteTodo = function(req, res) {
Todo.remove({
_id: req.params.todo_id
}, function(error, todo) {
if (error) {
res.send(error);
}
findAllTodosByUser(req, res);
});
}

Este controlador expone la API REST con la que se comunicará la pagina principal, haciendo llamadas ajax a los distintos métodos. En esta pieza se hace un require de otro script que representa el Model, en este caso es un document para MongoDB.

Model con mongoose

Aquí mostraré como se modeló el documento principal de la aplicación Todo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mongoose = require('mongoose');

var TodoSchema = new mongoose.Schema({
text: String,
date: {
type: Date,
default: Date.now
},
done: {
type: Boolean,
default: false
},
creator: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
});

var TodoModel = mongoose.model('Todo', TodoSchema);

module.exports = TodoModel;

Primero se incluye el módulo de mongoose que nos permitirá modelar los documentos para MongoDB. Luego se crea un schema para modelar y se le agregan los atributos que va a poseer y sus respectivas validaciones. Por ejemplo, el atributo date es de tipo Date y valor por omisión la fecha de hoy.

Además este schema contiene una referencia a otra Collection para dejar relacionado el usuario creador en sus Todos.

Route y manejo de URLs

Finalmente para que la aplicación funcioné, hay que indicarle a node.js que las urls que se soliciten hay que enviarlas a alguien que las atienda, en este caso, los controladores y sus funciones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app.get('/login', function(req, res) {
res.render('login', {});
});

app.post('/login', passport.authenticate('local', {
successRedirect: '/todos',
failureRedirect: '/login'
}));

app.get('/logout', function(req, res) {
req.logout();
res.redirect('/login');
});

app.get('/', function(req, res) {
res.redirect('/login')
})

app.get('/register', registerController.index);
app.post('/register', registerController.registerUser);

app.all('/api/*', security.ensureAuthenticated);
app.all('/todos', security.ensureAuthenticated);

app.get('/todos', indexController.index);
app.get('/api/todos', todosController.allTodos);
app.post('/api/todos', todosController.createTodo);
app.delete('/api/todos/:todo_id', todosController.deleteTodo);

No hay mucho que explicar, a buen programador pocas lineas de código, las urls que ahi aparecen son ruteadas a los controladores o se implementa el callback inline, como por ejemplo las urls de login y logout. Los controller que digan security o passport, serán materia del próximo post.

Quedan algunos temas sin tocar en este capítulo, falta que revisemos la integración con AngularJS, el despliegue de los templates con EJS y la integración con passport para autenticar y autorizar los request.

Si tienen preguntas, bienvenidas sean.