back to top

Un Esempio di applicazione Web realizzata con Express

In questa lezione vedremo un esempio di applicazione in Node.js realizzata con Express. Si tratta di un sito web in cui è possibile visualizzare la lista dei calciatori più costosi della storia. Gli utenti registrati potranno aggiungere un nuovo calciatore e scaricare il file in formato JSON contenente la lista aggiornata dei calciatori più costosi di sempre.

Per iniziare, useremo un file JSON contenente le informazioni relative al trasferimento di sei calciatori che salveremo all’interno di un database MongoDB. In questa lezione non potremo parlare in maniera esaustiva di MongoDB per il quale servirebbe un’intera guida. Creeremo invece un database su mLab dopo aver aperto un account gratuito con 500MB di spazio che useremo per familiarizzare con MongoDB. (Una soluzione alternativa a mLab è rappresentata da MongoDB Atlas) In Express è possibile usare un gran numero di database diversi, abbiamo scelto MongoDB perché è uno dei database NoSQL più popolari e interessanti. Nonostante MongoDB fornisca un driver ufficiale per Node.js, in questo esempio useremo Mongoose. Mongoose è un ODM (Object Data Model) che consente di lavorare con i dati che salviamo o preleviamo dal database in termini di oggetti Javascript.

Rapida introduzione a MongoDB

MongoDB è un database open source ed è uno dei database più usati in Node.js. Così come in MySQL, in MongoDB possiamo creare diversi database. Ogni database può contenere varie Collezioni (pensate alle tabelle di MySQL) di Documenti (sono come le righe di una tabella in MySQL). MongoDB salva nelle collezioni i dati in un formato che è simile al formato JSON. Al contrario di MySQL, un documento non deve per forza avere una struttura fissa. Proprio per questo motivo è possibile creare una nuova collezione inserendo il primo documento senza definire la struttura della collezione stessa. In MongoDB un Documento può contenere al suo interno dei documenti annidati. Se volete avere maggiori informazioni e dettagli su MongoDB vi consiglio comunque di consultare la documentazione ufficiale.

Creare e configurare un database su mLab

Proseguiamo con il nostro esempio. Creiamo un account gratuito su mLab e dopo aver effettuato l’accesso, all’interno della nostra dashboard creiamo un nuovo database.

creare un nuovo database in mLab

Nella schermata successiva chiediamo a mLab di creare una Sandbox gratuita su Amazon Web Services.

processo creazione database mlab e scelta del piano gratuito

Selezioniamo quindi la regione come mostrato nell’immagine in basso e proseguiamo al passo successivo.

processo creazione database mlab scelta regione

Scegliamo infine un nome per il nostro database. Nel caso specifico lo nomineremo ‘football’.

processo creazione database mlab selezione database

Controlliamo che tutti i dettagli siano corretti e inviamo l’ordine. (Ricordiamo che è completamente gratuito e non è necessario inserire i dati di una carta di credito)

processo creazione database mlab conferma ordine

Dovremo aspettare che mLab completi la configurazione del nuovo database creato. Terminato questo processo, potremo selezionare il nuovo database all’interno della dashboard.

processo creazione database selezione nuovo database creato

Nella nuova schermata, selezioniamo la scheda ‘Users’ e aggiungiamo poi un nuovo utente.

Completiamo il form per creare il nuovo utente del quale utilizzeremo username e password per accedere al database in Express.

processo creazione database mlab creazione utente

Configurazione dell’applicazione Express

Dopo aver creato il database su mLab, possiamo iniziare a lavorare alla nostra applicazione in Express. In questo esempio creeremo la nostra applicazione partendo da zero, ma Express mette a disposizione un tool estremamente utile per inizializzare un progetto. Si tratta di Express generator che possiamo installare globalmente col seguente comando.

npm install express-generator -g

Potete trovare delle informazioni dettagliate sulla documentazione ufficiale di Express.

Continuando col nostro esempio, creiamo una nuova cartella, ci spostiamo al suo interno e lanciamo il comando npm init -y per generare il file package.json che modificheremo come mostrato in basso.

{
  "name": "simple_express_example",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^1.0.3",
    "body-parser": "^1.18.2",
    "connect-mongo": "^2.0.0",
    "cookie-parser": "^1.4.3",
    "ejs": "^2.5.7",
    "express": "^4.14.2",
    "express-session": "^1.15.6",
    "format-number": "^3.0.0",
    "moment": "^2.19.1",
    "mongoose": "^4.12.1",
    "serve-favicon": "^2.4.5"
  }
}

Come potete vedere abbiamo installato una serie di dipendenze che useremo nel resto del progetto.

npm install --save bcrypt 
   body-parser connect-mongo cookie-parser ejs 
   express express-session format-number  
   moment mongoose serve-favicon

Iniziamo a vedere quale sarà la struttura dell’applicazione Express lanciando il seguente comando.

tree

.
├── app.js
├── configs.js
├── controllers
│   ├── indexRoutesController.js
│   └── usersRoutesController.js
├── databaseConfig.js
├── helpers
│   └── login.js
├── middlewares
│   ├── configAuthMiddleware.js
│   ├── configClearErrorMiddleware.js
│   └── sessionMiddlewares.js
├── model
│   ├── calciatori.json
│   ├── player.js
│   └── user.js
├── node_modules
├── package-lock.json
├── package.json
├── populateDB.js
├── public
│   ├── football.ico
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       ├── pure-min.css
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── calciatori.ejs
    ├── error.ejs
    ├── index.ejs
    ├── login.ejs
    ├── nuovo-calciatore.ejs
    ├── partials
    │   ├── error.ejs
    │   ├── form.ejs
    │   ├── head.ejs
    │   └── header.ejs
    └── register.ejs

Partiamo dal file configs.js nel quale inseriamo alcune informazioni che ci serviranno per l’accesso al database (si tratta del nome utente e password che abbiamo specificato per il database ‘football’) e la configurazione di Express-session.

// file: configs.js

const dbUser = 'claudio';
const password = 'password-super-sicura';

const sessionSecret = 'Super Secret';
const sessionName = 'sessionId';

module.exports = { dbUser, password, sessionSecret, sessionName };

Passiamo quindi al file databaseConfig.js in cui importiamo Mongoose e le credenziali per connettersi al database ospitato su mLab.

const mongoose = require('mongoose');
const configs = require('./configs');

const user = configs.dbUser;
const password = configs.password;

const address = `mongodb://${user}:${password}@ds113935.mlab.com:13935/football`;

mongoose.Promise = Promise;

mongoose.connect(address, { useMongoClient: true });

const db = mongoose.connection;

db.on('error', (error) => console.log('Errore connessione' + error));

module.exports = db;

Configuriamo Mongoose in modo da usare l’oggetto Promise nativo e effettuiamo il collegamento al database remoto usando la nuova modalità di connessione prevista da Mongoose. ({ useMongoClient: true })

Analizziamo quindi il contenuto della cartella model. In particolare ci concentriamo per ora sul file player.js. Nonostante MongoDB permetta di inserire in un database dei documenti con una struttura flessibile, usiamo Mongoose per definire un preciso schema per ogni documento che andremo ad aggiungere alla Collezione ‘players’.

// file: model/player.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const PlayerSchema = new Schema({
  nome: {
    type: String,
    required: true,
    trim: true
  },
  ruolo: {
    type: String,
    required: true,
    enum: ['Portiere', 'Difensore', 'Centrocampista', 'Attaccante']
  },
  nazione: {
    type: String,
    required: true,
    trim: true
  },
  dataNascita: Date,
  prezzo: Number,
  squadraOrigine: {
    campionato: String,
    nazione: String,
    nomeSquadra: String
  },
  squadraDestinazione: {
    campionato: String,
    nazione: String,
    nomeSquadra: String
  }
});

PlayerSchema.index({prezzo: -1});

const Player = mongoose.model('player', PlayerSchema);

module.exports = Player;

Come potete vedere dal codice, abbiamo importato Mongoose e abbiamo creato un nuovo Schema. In questo modo definiamo in maniera univoca quale deve essere la struttura di ogni documento della collezione ‘players’. (Notate che non sarà necessario specificare il nome di un collezione, Mongoose userà il nome del modello al plurale. In questo caso Mongoose creerà una collezione ‘players’ contenente dei documenti aventi la stessa struttura definita da PlayerSchema) In particolare ogni documento avrà un ‘nome’ di tipo Stringa che è obbligatorio. Mongoose provvederà a rimuovere eventuali spazi presenti prima o dopo il nome. (trim: true). Sarà sempre presente un campo ‘ruolo’ che può assumere uno dei valori espressi nell’array ‘enum’. Per ogni calciatore salveremo anche delle informazioni relative alla sua data di nascita che deve essere di tipo ‘Date’ e il prezzo del trasferimento. Ciascun documento contiene anche le informazioni sulla squadra che ha ceduto il giocatore e su quella che l’ha acquistato. Si tratta di due oggetti che devono contenere tre proprietà (campionato, nazione, nomeSquadra). A partire dallo Schema ‘PlayerSchema’ costruiamo il modello ‘Player’ che assume un ruolo simile a una classe dei linguaggi di programmazione orientati agli oggetti. Infatti, a partire dal modello ‘Player’ creeremo le varie istanze che saranno degli oggetti i quali rappresentano i diversi documenti che andremo a inserire nella collezione ‘players’. In Mongoose usiamo solitamente un file per ogni Schema/Modello che definiamo.

È venuto quindi il momento di aggiungere dei documenti nella collezione ‘players’ che verrà creata contestualmente all’inserimento del primo documento. Per non inserire ogni singolo documento manualmente, usiamo il file ‘calciatori.json’ nella cartella ‘model’. Tale file contiene le informazioni aggiornate relative al trasferimento dei sei calciatori più costosi di sempre.

[
  {
    "nome": "Neymar",
    "ruolo": "Attaccante",
    "dataNascita": "1992-02-05T00:00:00.000Z",
    "prezzo": 222000000,
    "nazione": "Brasile",
    "squadraDestinazione": {
      "campionato": "Ligue 1",
      "nazione": "Francia",
      "nomeSquadra": "Paris Saint-Germain"
    },
    "squadraOrigine": {
      "campionato": "La Liga",
      "nazione": "Spagna",
      "nomeSquadra": "Barcellona"
    }
  },
  {
    "nome": "Kylian Mbappé",
    "ruolo": "Attaccante",
    "dataNascita": "1998-12-20T00:00:00.000Z",
    "prezzo": 180000000,
    "nazione": "Francia",
    "squadraDestinazione": {
      "campionato": "Ligue 1",
      "nazione": "Francia",
      "nomeSquadra": "Paris Saint-Germain"
    },
    "squadraOrigine": {
      "campionato": "Ligue 1",
      "nazione": "Francia",
      "nomeSquadra": "Monaco"
    }
  },
  {
    "nome": "Paul Pogba",
    "ruolo": "Centrocampista",
    "dataNascita": "1993-03-15T00:00:00.000Z",
    "prezzo": 105000000,
    "nazione": "Francia",
    "squadraDestinazione": {
      "campionato": "Premier League",
      "nazione": "Inghilterra",
      "nomeSquadra": "Manchester United"
    },
    "squadraOrigine": {
      "campionato": "Serie A",
      "nazione": "Italia",
      "nomeSquadra": "Juventus"
    }
  },
  {
    "nome": "Ousmane Dembélé",
    "ruolo": "Attaccante",
    "dataNascita": "1997-05-15T00:00:00.000Z",
    "prezzo": 105000000,
    "nazione": "Francia",
    "squadraDestinazione": {
      "campionato": "La Liga",
      "nazione": "Spagna",
      "nomeSquadra": "Barcellona"
    },
    "squadraOrigine": {
      "campionato": "Bundesliga",
      "nazione": "Germania",
      "nomeSquadra": "Borussia Dortmund"
    }
  },
  {
    "nome": "Gareth Bale",
    "ruolo": "Attaccante",
    "dataNascita": "1989-07-14T00:00:00.000Z",
    "prezzo": 100000000,
    "nazione": "Galles",
    "squadraDestinazione": {
      "campionato": "La Liga",
      "nazione": "Spagna",
      "nomeSquadra": "Real Madrid"
    },
    "squadraOrigine": {
      "campionato": "Premier League",
      "nazione": "Inghilterra",
      "nomeSquadra": "Tottenham Hotspur"
    }
  },
  {
    "nome": "Cristiano Ronaldo",
    "ruolo": "Attaccante",
    "dataNascita": "1985-02-05T00:00:00.000Z",
    "prezzo": 94000000,
    "nazione": "Portogallo",
    "squadraDestinazione": {
      "campionato": "La Liga",
      "nazione": "Spagna",
      "nomeSquadra": "Real Madrid"
    },
    "squadraOrigine": {
      "campionato": "Premier League",
      "nazione": "Inghilterra",
      "nomeSquadra": "Manchester United"
    }
  }
]

Avendo il file ‘calciatori.json’ a disposizione, possiamo importarlo nel database ‘football’ e creare la collezione ‘players’ con il seguente comando.

mongoimport -h ds227325.mlab.com:27325  
-d football -c players -u claudio -p password-super-sicura  
--file ./model/calciatori.json --jsonArray

Il problema del comando mongoimport è che può verificarsi che, importando un file, non venga mantenuto il tipo dei dati in maniera corretta. Nel nostro caso siamo riusciti comunque a creare tutti i documenti in modo corretto. In alternativa possiamo usare Node.js per importare i documenti come mostrato nel file populateDB.js.

const db = require('./databaseConfig');
const Player = require('./model/player');
const calciatori = require('./model/calciatori.json');

async function populateDB () {
  if (db.collections.players) {
    try {
      await Player.remove({});
      await Player.create(calciatori);
    } catch (error) {
      console.log(error);
    }
  }
}

populateDB()
  .then(() => db.close())
  .then(() => console.log('connection closed'));

Nel frammento di codice riportato sopra, importiamo il modello Player creato nel file model/player.js e il file model/calciatori.json. La costante calciatori contiene un array in cui ogni elemento è un oggetto Javascript. Eseguiamo quindi la funzione populateDB() che controlla se è già presente una collezione ‘players’. In tal caso rimuoviamo tutti i documenti già presenti e usiamo il metodo Player.create(calciatori) per inserire nella collezione ‘players’ (viene creata da MongoDB all’inserimento del primo documento) i documenti relativi ai calciatori presenti nel file JSON. Terminata questa operazione, chiudiamo la connessione.

Ritornando alla cartella model, vediamo l’ultimo file rimasto. (model/user.js)

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;

const UserSchema = new Schema({
  username: {
    type: String,
    unique: true,
    required: true,
    trim: true
  },
  password: {
    type: String,
    required: true
  }
});

UserSchema.pre('save', function (next) {
  bcrypt.hash(this.password, SALT_ROUNDS)
    .then((hash) => {
      this.password = hash;
      next();
    })
    .catch((reason) => next(reason));
});

UserSchema.statics.authenticate = function (username, password) {
  return User
    .findOne({ username })
    .exec()
    .then(user => {
      if (user) {
        const hashMatch = bcrypt.compare(password, user.password);
        return {user, hashMatch};
      }
      return {user: null, hashMatch: false};
    });
};

const User = mongoose.model('User', UserSchema);

module.exports = User;

In questo caso definiamo uno schema ‘UserSchema’ per salvare le credenziali di accesso degli utenti in una collezione ‘users’. (Utilizzeremo il metodo user.save() per aggiungere un documento contentente le informazioni di ogni utente alla collezione ‘users’). In questo modulo abbiamo anche importato bcrypt che Mongoose userà prima di inserire un documento nella collezione per generare l’hash della password in modo da salvarla nel database in maniera sicura. Abbiamo inoltre definito un metodo statico per il modello User (User.authenticate) che verrà utilizzata quando un utente tenterà di effettuare il login. Infatti, controlliamo dapprima se l’utente è presente nella collezione ‘users’. Se l’utente viene trovato, confrontiamo l’hash della password digitata nel form e inviata al server con quella salvata nel database in fase di registrazione e restituiamo un oggetto contente i dati dell’utente e una Promise che permetterà di verificare se la password è corretta e possiamo consentire all’utente di effettuare l’accesso alla sua area privata.

I file template della cartella views

Nella directory views inseriamo i file template dell’applicazione. Si tratta di file EJS che abbiamo scelto come motore di template nel file app.js. All’interno di views abbiamo inserito la directory partials contente dei frammenti di template che includiamo con la funzione include() negli altri file.ejs.

.
├── calciatori.ejs
├── error.ejs
├── index.ejs
├── login.ejs
├── nuovo-calciatore.ejs
├── partials
│   ├── error.ejs
│   ├── form.ejs
│   ├── head.ejs
│   └── header.ejs
└── register.ejs

Vediamo allora il contenuto del file index.ejs corrispondente alla pagina iniziale del sito.

pagina iniziale sito calciatori
// views/index.ejs

<!doctype html>
<html lang="en">
<%- include('partials/head.ejs', {title: 'I Calciatori più pagati della storia'}); %>
<body>

  <%- include('partials/header.ejs'); %>

  <div class="splash-container">
    <div class="splash">
      <p class="splash-subhead">
        Visualizza la lista dei calciatori pi&ugrave; costosi di sempre.
      </p>
      <p>
        <a href="/calciatori" class="pure-button pure-button-primary">Vai alla lista</a>
      </p>
    </div>
  </div>

</body>
</html>

Abbiamo incluso i file partials/head.ejs e partials/header.ejs. Nel caso del primo file abbiamo passato come argomento un oggetto contente la proprietà ‘title’ che viene usata nel file head.ejs.

// file views/partials/head.ejs

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %></title>

<link rel="stylesheet" href="/static/stylesheets/pure-min.css" >
<link rel="stylesheet" href="/static/stylesheets/style.css">
</head>

Nel file head.ejs usiamo la proprietà ‘title’ ricevuta per il titolo della pagina. Importiamo inoltre i fogli di stile stye.css, contenente le regole da noi specificate, e pure-min.css che contiene un set di regole css che potremo usare nella nostra applicazione. Pure.css è un progetto interessante, simile a Bootstrap, che, in poco meno di 4KB, consente di definire lo stile delle pagine di un sito. Pure.css è costituito da diversi moduli. Il modulo Grids, per esempio, consente di creare un design responsive per un sito web in maniera semplice e intuitiva.

Quello che segue è invece il file header.ejs in cui inseriamo i link alla pagina per effettuare il login o la registrazione. Tuttavia, se un utente ha già effettuato il login, mostriamo il suo nome e il link per effettuare il logout.

// file views/partials/header.ejs

<div class="header">
  <div class="home-menu pure-menu pure-menu-horizontal pure-menu-fixed">
    <a class="pure-menu-heading" href="/">Calciatori</a>

    <ul class="pure-menu-list">
      <% if (locals.user) { %>
        <li class="pure-menu-item pure-menu-selected">
          Ciao, <%= (locals.user) %>
        </li>
        <li class="pure-menu-item">
          <a href="/utenti/logout" class="pure-menu-link">Logout</a>
        </li>
      <% } else { %>
        <li class="pure-menu-item pure-menu-selected">
          <a href="/utenti/login" class="pure-menu-link">Login</a>
        </li>
        <li class="pure-menu-item">
          <a href="/utenti/registrati" class="pure-menu-link">Registrati</a>
        </li>
      <% } %>
    </ul>
  </div>
</div>

I due file views/login.ejs e views/register.ejs sono simili. In entrambi infatti importiamo i file views/partials/form.ejs e views/partials/error.ejs e mostriamo un form se l’utente non ha ancora effettuato l’accesso. In caso contrario inseriamo un link alla pagina iniziale. Tutti gli utenti, che non hanno ancora effettuato l’accesso al sito e che tentano di accedere alla pagina in cui si può aggiungere un calciatore, verranno rediretti alla pagina per effettuare il login.

accesso negato creazione nuovo calciatore
// file views/login.ejs

<!doctype html>
<html lang="en">
  <%- include(
      'partials/head.ejs', 
      {title: 'Effettua l'accesso per all tua area privata'}
    ); 
  %>
<body>
  <%- include('partials/header.ejs'); %>
  <% if (!locals.user) { %>
  <main role="main" class="container">
  <%- include('partials/form.ejs', {h1: 'Inserisci le tue credenziali', button: 'Login'}); %> 
  <%- include('partials/error.ejs', {action: 'login'})%>
  <% } else { %>
    <div class="content">
      <a href="/">Torna alla Pagina Iniziale</a>
      <p>Ciao, <%= locals.user %></p>
    </div>
  <% } %>
  </main>
</body>
</html>
login form con credenziali errate

Se le credenziali di accesso non sono corrette verrà mostrato un messaggio d’errore così come specificato nel file views/partials/error.ejs. Nell’applicazione, infatti, aggiungiamo, in caso di errore, un oggetto error ad app.locals. Quest’ultimo sarà poi accessibile in qualsiasi view dell’applicazione attraverso l’oggetto locals. Facendo riferimanento al file views/partials/error.ejs, la variabile action viene passata dal template login.ejs o register.ejs e assume rispettivamente il valore ‘login’ e ‘register’. L’oggetto app.locals.error può avere infatti più proprietà a seconda della pagina in cui si verifica un certo errore.

// file views/partials/error.ejs

<% if (locals.error && locals.error[action]) { %>
  <div class="error-message">
    <p>
      <%= locals.error[action].msg %>
    </p>
  </div>
<% } %>
// file views/register.ejs

<!doctype html>
<html lang="en">
  <%- include(
      'partials/head.ejs', 
      {title: 'Registrati per aggiungere nuovi giocatori'}
    ); 
  %>
<body>
    <%- include('partials/header.ejs'); %>
    <% if (!locals.user) { %>
    <main role="main" class="container">
    <%- include('partials/form.ejs', {h1: 'Registrati', button: 'Registrati'}); %> 
    <%- include('partials/error.ejs', {action: 'register'})%>
    <% } else { %>
      <div class="content">
        <a href="/">Torna alla Pagina Iniziale</a>
        <p>Ciao, <%= locals.user %></p>
      </div>
    <% } %>
    </main>

</body>
</html>
form registrazione sito calciatori

Di seguito riportiamo il template views/partials/form.ejs incluso in entrambi i file. (register.ejs e login.ejs)

// file views/partials/form.ejs

<h1><%= h1 %></h1>
<div class="form-wrapper wrapper">
  <form method="post" class="pure-form pure-form-stacked">
    <fieldset>
      <legend>Compila tutti i campi sottostanti</legend>
      <label for="username">Username</label>
      <input id="username" name="username" type="text" placeholder="Username" required>
      <label for="password">Password</label>
      <input id="password" name="password" type="password" placeholder="Password" required>
      <button 
        type="submit" 
        class="pure-button pure-button-primary"><%= button %></button>
    </fieldset>
  </form>
</div>

I soli utenti che hanno effettuato il login potranno poi accedere alla pagina che permette di aggiungere un nuovo calciatore alla lista.

// file views/nuovo-calciatore.ejs

<!doctype html>
<html lang="en">
  <%- include(
      'partials/head.ejs', 
      {title: 'Aggiungi un calciatore alLa lista dei calciatori più pagati della storia'}
    ); 
  %>
<body>
  <%- include('partials/header.ejs'); %>

  <main role="main" class="container">
    <h1>Aggiungi un nuovo calciatore</h1>
    <div class="form-wrapper wrapper">
      <form method="post" class="pure-form pure-form-stacked">
        <fieldset>
          <legend>Compila i campi del form sottostante</legend>

            <div class="pure-g">
              <div class="pure-u-1-2">
                <label for="nome">Nome Calciatore</label>
                <input id="nome" name="nome" class="pure-u-23-24" type="text" required>
              </div>

              <div class="pure-u-1-2">
                <label for="dataNascita">Data di Nascita</label>
                <input id="dataNascita" name="dataNascita" class="pure-u-23-24" type="date" required>
              </div>

              <div class="pure-u-1-2">
                <label for="nazione">Nazione</label>
                <input id="nazione" name="nazione" class="pure-u-23-24" type="text" required>
              </div>

              <div class="pure-u-1-2">
                <label for="prezzo">Prezzo</label>
                <input id="prezzo" name="prezzo" class="pure-u-23-24" type="number" required>
              </div>

              <div class="pure-u-1-2">
                <label for="squadraOrigine">Ceduto da</label>
                <input 
                id="squadraOrigine"
                name="squadraOrigine" 
                class="pure-u-23-24" 
                type="text" 
                placeholder="Nome squadrea - nazione - campionato" required>
              </div>

              <div class="pure-u-1-2">
                <label for="squadraDestinazione">Acquistato da</label>
                <input 
                  id="squadraDestinazione"
                  name="squadraDestinazione"
                  class="pure-u-23-24" 
                  type="text" 
                  placeholder="Nome squadrea - nazione - campionato" required>
              </div>

              <div class="pure-u-1-2">
                <label for="ruolo">Ruolo</label>
                <select id="ruolo" name="ruolo" class="pure-input-1-2">
                <option value="Portiere">Portiere</option>
                <option value="Difensore">Difensore</option>
                <option value="Centrocampista">Centrocampista</option>
                <option value="Attaccante">Attaccante</option>
                </select>
              </div>
            </div>
            <br>
            <button type="submit" class="pure-button pure-button-primary">Invia</button>
        </fieldset>
      </form>
    </div> 
  </main>


</body>
</html>
form per aggiungere un calciatore

Infine la lista dei calciatori, è inserita nel file views/calciatori.ejs.

// file views/calciatori.ejs

<!doctype html>
<html lang="en">
  <%- include(
      'partials/head.ejs', 
      {title: 'La lista dei calciatori più pagati della storia'}
    ); 
  %>
<body>
  <%- include('partials/header.ejs'); %>

  <main role="main" class="container">
    <h1>Lista dei calciatori pi&ugrave; costosi della storia</h1>
    <div class="players-wrapper wrapper">
      <% calciatori.forEach(function(calciatore){ %>
      <div class="player">
        <h2><%= calciatore.nome %></h2>
        <table class="players-data">
          <tbody>
            <tr>
              <th>Ruolo</th>
              <td><%= calciatore.ruolo %></td>
            </tr>
            <tr>
              <th>Data di nascita</th>
              <td><%= moment(calciatore.dataNascita).format("DD/MM/YYYY") %></td>
            </tr>
            <tr>
              <th>Nazione</th>
              <td><%= calciatore.nazione %></td>
            </tr>
            <tr>
              <th>Ceduto da</th>
              <td><%= calciatore.squadraOrigine.nomeSquadra %></td>
              <td><%= calciatore.squadraOrigine.campionato %></td>
              <td><%= calciatore.squadraOrigine.nazione %></td>
            </tr>
            <tr>
              <th>Squadra acquirente</th>
              <td><%= calciatore.squadraDestinazione.nomeSquadra %></td>
              <td><%= calciatore.squadraDestinazione.campionato %></td>
              <td><%= calciatore.squadraDestinazione.nazione %></td>
            </tr>
            <tr>
              <th>Prezzo</th>
              <td><%= customNumberFormat(calciatore.prezzo) %></td>
            </tr>

          </tbody>
        </table>
      </div>
      <% }); %>
    </div>
    <div class="action pure-g">
      <div class="add-player pure-u-1-2">
        <a class="pure-button button-primary" href="/nuovo-calciatore">Aggiungi un nuovo calciatore</a>
      </div>
      <div class="download-json pure-u-1-2">
        <a class="button-secondary pure-button" href="/calciatori.json">Scarica il file in formato JSON</a>
      </div>
    </div>
  </main>


</body>
</html>
lista dei calciatori più costosi della storia

Il file app.js

Il file app.js è il file principale dell’applicazione in cui configuriamo e colleghiamo le diverse parti insieme. All’interno di questo file avviamo anche il server HTTP dell’applicazione che, se non specificato diversamente, resterà in ascolto sulla porta 7777.

// file: app.js

const express = require('express');
const http = require('http');
const path = require('path');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const MongoStore = require('connect-mongo')(session);
const favicon = require('serve-favicon');
const db = require('./databaseConfig');
const { sessionSecret, sessionName } = require('./configs');
const indexRoutesController = require('./controllers/indexRoutesController');
const usersRoutesController = require('./controllers/usersRoutesController');

const app = express();

const PORT = 7777 || process.env.PORT;

const server = http.createServer(app);

// importa i router per i diversi percorsi
const indexRoutes = require('./routes/index')(indexRoutesController);
const usersRoutes = require('./routes/users')(usersRoutesController);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// set favicon
app.use(favicon(path.join(__dirname, 'public', 'football.ico')));

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));

// parse application/json
app.use(bodyParser.json());

// parse cookies
app.use(cookieParser());

app.use(session({
  secret: sessionSecret,
  resave: false,
  name: sessionName,
  saveUninitialized: false,
  store: new MongoStore({
    mongooseConnection: db,
    touchAfter: 24 * 3600
  })
}));

// static assets
app.use('/static', express.static(path.join(__dirname, 'public')));

// percorsi dell'applicazione
app.use('/', indexRoutes);
app.use('/utenti', usersRoutes);

// intercetta l'errore 404 e lo manda all'error handler
app.use(function (req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function (err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};
  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

server.listen(PORT, function () {
  const host = server.address().address;
  const port = server.address().port;
  const address = `http://${host}:${port}`;
  console.log(`Server in ascolto all'indirizzo: ${address}`);
});

Nel file app.js importiamo tutti i moduli necessari e due differenti oggetti Router. Useremo un particolare router per tutti i percorsi che iniziano per ‘/utenti’. Il motivo è che abbiamo preferito isolare le funzionalità di registrazione, login e logout dal resto delle pagine usando anche due differenti controller. Abbiamo quindi settato EJS come motore di template e usiamo il package serve-icon per inviare al browser una favicon personalizzata. Per servire i contenuti statici usiamo il middleware express.static e definiamo alcuni middleware a livello di pagina per la gestione degli errori. Per motivi di sicurezza, mostreremo nel browser delle informazioni relative agli errori solo in fase di sviluppo. Inoltre, importiamo e configuriamo il middleware body-parser per poter ricevere ed elaborare correttamente i dati inviati da un utente. (dati inviati tramite il form di login o registrazione o quando viene aggiunto un nuovo calciatore alla lista). Usiamo infine i middleware cookie-parser e express-session per implementare le funzionalità di login attraverso l’uso delle sessioni. I dati relativi all’accesso degli utenti saranno salvati nel database ‘football’ in un’apposita collezione. Per far ciò usiamo MongoStore e riutilizziamo la connessione al database già avviata da Mongoose.

I due Router e rispettivi controller

Nel file routes/index.js abbiamo definito la funzione in cui vengono gestiti i percorsi della pagina iniziale e quelli relativi alla visualizzazione e modifica della lista dei calciatori.

// file: routes/index.js

const express = require('express');
const isLoggedIn = require('../middlewares/sessionMiddlewares');
const router = express.Router();

function indexRoutes (controller) {
  // Valida per il percorso /   
  router.get('/', controller.indexPage);

  // Valida per il percorso /giocatori
  router.get('/calciatori', controller.playersPage);

  // Usata per scaricare il file .json originale
  router.get('/calciatori.json', isLoggedIn, controller.playersFile);

  // Valida per il percorso /giocatori
  router.route('/nuovo-calciatore')
    .all(isLoggedIn)
    .get(controller.newPlayerGET)
    .post(controller.newPlayerPOST);

  return router;
};

module.exports = indexRoutes;

All’interno del file app.js passiamo un oggetto controller alla funzione indexRoutes. Tale oggetto presenta tutti i metodi che impieghiamo per i diversi percorsi e tipo di richiesta HTTP ricevuta dal Client. Isolare questa funzionalità in un oggetto separato, permette di migliorare l’organizzazione e la leggibilità del codice e semplificare la fase di test dell’applicazione. Potremo infatti scrivere ed eseguire dei test per ogni metodo di ogni controller. Notate che, per accedere ai percorsi ‘/nuovo-giocatore’ e ‘/calciatori.json’, è necessario essere registrati e aver effettuato il login. Per limitare l’accesso a tali aree del sito usiamo il middleware isLoggedIn definito nel file middlewares/sessionMiddlewares.js.

Il middleware isLoggedIn

La funzione middleware isLoggedIn è usata per limitare l’accesso a determinate aree del sito. Se la proprietà req.session.userID è settata, ovvero se l’utente ha effettuato l’accesso, invochiamo la funzione next() che passa il controllo al middleware successivo. In caso contrario aggiungiamo all’oggetto app.locals.error una proprietà contenente un messaggio d’errore e redirigiamo l’utente alla pagina in cui è possibile effetture il login.

// file: middlewares/sessionMiddlewares.js

function isLoggedIn (req, res, next) {
  if (req.session && req.session.userID) {
    return next();
  } else {
    const target = req.app.locals.error || {};
    Object.assign(target,
      {
        login: {
          msg: 'Area protetta. Per accedere devi eseguire il login',
          isErrorValid: true
        }
      });
    req.app.locals.error = target;
    res.redirect('/utenti/login');
  }
}

module.exports = isLoggedIn;

Il controller indexRoutesController

Nel file controllers/indexRoutesController.js definiamo le funzioni che usiamo poi nel file routes/index.js

// file: controllers/indexRoutesController.js

const path = require('path');
const fs = require('fs');
const { promisify } = require('util');
const moment = require('moment');
const formatNumber = require('format-number');
const Player = require('../model/player');

const writeFileAsync = promisify(fs.writeFile);

const customNumberFormat = formatNumber({
  prefix: '€',
  integerSeparator: '.',
  decimal: ','
});

// Controller per la homepage
function indexPage (req, res) {
  res.render('index');
}

// Controller per la lista dei giocatori (percorso /calciatori)
function playersPage (req, res, next) {
  Player.find({}, '-_id')
    .sort({'prezzo': -1})
    .exec()
    .then((calciatori) => {
      res.render('calciatori', {calciatori, moment, customNumberFormat});
    })
    .catch(error => {
      next(error);
    });
}

// Controller per GET requests /nuovo-calciatore
function newPlayerGET (req, res) {
  res.render('nuovo-calciatore');
}

// Controller per POST requests /nuovo-calciatore
function newPlayerPOST (req, res, next) {
  const separator = /s*-s*/;
  const filePath = path.join(__dirname, '../model', 'calciatori.json');
  let campionato, nazione, nomeSquadra;

  if (!req.body) return res.sendStatus(400);

  let player = req.body;

  player.dataNascita = new Date(player.dataNascita);

  [campionato, nazione, nomeSquadra] = player.squadraOrigine.split(separator);
  player.squadraOrigine = {campionato, nazione, nomeSquadra};

  [campionato, nazione, nomeSquadra] = player.squadraDestinazione.split(separator);
  player.squadraDestinazione = {campionato, nazione, nomeSquadra};

  player = new Player(player);

  player.save()
    .then((player) => {
      return Player.find({}, '-_id -__v')
        .sort({'prezzo': -1})
        .exec();
    })
    .then((calciatori) => {
      return writeFileAsync(filePath, JSON.stringify(calciatori, null, 2));
    })
    .then(() => res.redirect('/calciatori'))
    .catch(error => next(error));
}

// avvia il download del file calciatori.json
function playersFile (req, res, next) {
  const filePath = path.join(__dirname, '../model', 'calciatori.json');
  // se il file esiste, invialo al client
  fs.access(filePath, fs.constants.F_OK, (error) => {
    if (error) {
      // passa l'errore al middleware 'error handler' in app.js
      next(error);
    }
    res.download(filePath, 'Calciatori.json');
  });
}

module.exports = {
  indexPage,
  playersPage,
  newPlayerGET,
  newPlayerPOST,
  playersFile
};

Per la pagina iniziale ci limitiamo a mostrare il contenuto del file views/index.ejs. Per il percorso ‘/calciatori’, usiamo la funzione playersPage in cui recuperiamo tutti i documenti contenuti nella collezione players del database football. Otteniamo un array di calciatori che contiene appunto degli oggetti con le informazioni dei calciatori. A tali oggetti abbiamo rimosso la proprietà univoca ‘_id’ aggiunta da MongoDB in fase di inserimento dei documenti nella collezione. L’array calciatori è ordinato secondo il valore della proprietà numerica prezzo in ordine decrescente. Mostriamo quindi il contenuto del template views/calciatori.ejs a cui abbiamo passato l’array dei calciatori oltre alle funzioni moment e customNumberFormat che useremo per la formattazione delle date di nascita e dei prezzi dei calciatori all’interno de file calciatori.ejs.

Con la funzione playersFile permettiamo al Client di scaricare il file calciatori.json. Usiamo la funzione fs.access() per verificare che il file esista e in tal caso usiamo la funzione definita in Express res.download() che avvierà il download del file. Il Client visualizzerà una finestra di dialogo in cui scegliere la cartella in cui salvare il file sul suo dispositivo.

Per il percorso ‘/nuovo-calciatore’ usiamo due funzioni diverse a seconda del tipo di richiesta. Per ogni richiesta di tipo GET, mostriamo una pagina HTML generata a partire dal template views/nuovo-calciatore.ejs. Per le richieste di tipo POST, usiamo invece la funzione newPlayerPOST che estrae dall’oggetto req.body i dati relativi a un nuovo calciatore, inviati dal Client dopo aver compilato il relativo form. Viene quindi creata una nuova istanza di Player e si salva il nuovo calciatore nel database. Viene poi recuperata la lista aggiornata dei calciatori che viene copiata all’interno del file calciatori.json il quale contiene così le informazioni aggiornate. L’utente viene infine rediretto alla pagina in cui potrà visualizzare la lista dei calciatori comprensiva dell’ultimo calciatore inserito.

Analizziamo ora l’altro file presente nella cartella routes. Si tratta del file users.js in cui, con l’ausilio del controller controllers/usersRoutesController, permettiamo all’utente di effettuare il login, il logout e la registrazione al sito. In questo caso usiamo anche dei middleware configurabili in modo tale da non dover ripetere del codice che può essere condiviso dai due percorsi /utenti/login e /utenti/registrati.

// file routes/users.js

const express = require('express');
const router = express.Router();

const configClearErrorMiddleware =
  require('../middlewares/configClearErrorMiddleware');

const {
  configErrorAuthMiddleware,
  configAuthMiddleware
} = require('../middlewares/configAuthMiddleware');

function usersRoutes (controller) {
  // Valida per il percorso /utenti/login   
  router.route('/login')
    .get(configClearErrorMiddleware('login'), controller.loginGET)
    .post(
      controller.loginPOST,
      configErrorAuthMiddleware('login'),
      configAuthMiddleware('/nuovo-calciatore')
    );

  // Valida per il percorso /utenti/registrati
  router.route('/registrati')
    .get(configClearErrorMiddleware('register'), controller.registerGET)
    .post(
      controller.registerPOST,
      configErrorAuthMiddleware(
        'register',
        'Impossibile eseguire la registrazione'),
      configAuthMiddleware('/nuovo-calciatore')
    );

  // Valida per /utenti/logout
  router.get('/logout', controller.logout);

  return router;
};

module.exports = usersRoutes;

Riportiamo il codice del controller di seguito per poi spiegare come funziona il tutto.

// file: controllers/usersRoutesController.js

const User = require('../model/user');
const { sessionName } = require('../configs');

function loginGET (req, res) {
  res.render('login');
};

function loginPOST (req, res, next) {
  if (req.body.username && req.body.password) {
    const userDetails = {...req.body};
    const error = new Error('Utente o password errati');
    error.status = 401;
    User.authenticate(userDetails.username, userDetails.password)
      .then(({user, hashMatch}) => {
        if (hashMatch.then && user) {
          hashMatch.then(result => {
            if (result) {
              req.user = user;
              next();
            } else {
              next(error);
            }
          });
        } else {
          return next(error);
        }
      })
      .catch(reason => next(reason));
  } else {
    const error = new Error('Completa correttamente tutti i campi');
    error.status = 400;
    return next(error);
  }
};

function registerGET (req, res) {
  res.render('register');
};

function registerPOST (req, res, next) {
  if (req.body.username && req.body.password) {
    const userDetails = {...req.body};

    User.create(userDetails)
      .then((user) => {
        req.user = user;
        next();
      })
      .catch((reason) => next(reason));
  } else {
    const error = new Error('Completa correttamente tutti i campi');
    error.status = 400;
    return next(error);
  }
};

function logout (req, res, next) {
  if (req.session) {
    req.session.destroy(function (errore) {
      if (errore) {
        return next(errore);
      } else {
        req.app.locals.user = '';
        res.clearCookie(sessionName);
        return res.redirect('/');
      }
    });
  }
}

module.exports = {
  loginGET,
  loginPOST,
  registerGET,
  registerPOST,
  logout
};

Per il percorso /utenti/logout usiamo la funzione omonima definita nel file controllers/usersRoutesController.js. Nel caso specifico, ci limitiamo a distruggere la sessione e, se non si verificano errori, rimuoviamo le informazioni dell’utente dall’oggetto app.locals.user e il cookie usato dalla sessione. Infine redirigiamo l’utente alla pagina principale.

Vediamo ora come si comporta l’applicazione per i percorsi /utenti/login e /utenti/registrati. Per quanto riguarda le richieste di tipo GET, ci limitiamo a mostrare una pagina in cui l’utente può inserire le proprie credenziali per effettuare il login oppure compilare il form per registrarsi al sito. In entrambi i casi usiamo il middleware restituito dalla funzione configClearErrorMiddleware(). Vediamo di cosa si tratta.

// file: middlewares/configClearErrorMiddleware

function configClearErrorMiddleware (page) {
  return function clearErrorMiddleware (req, res, next) {
    console.log(req.app.locals.error);
    if (req.app.locals.error && req.app.locals.error[page]) {
      const isErrorValid = req.app.locals.error[page].isErrorValid;
      if (isErrorValid) {
        req.app.locals.error[page].isErrorValid = false;
      } else {
        req.app.locals.error = {};
      }
    }
    next();
  };
}

module.exports = configClearErrorMiddleware;

La funzione riportata sopra restituisce un middleware che usiamo per rimuovere eventuali errori dall’oggetto app.locals.error. Infatti, ogni volta che si verifica un errore, inseriamo le informazioni in questo oggetto per mostrarle a video sotto il form in cui l’utente ha inserito username e password. Usando il middleware, vogliamo fare in modo che, se l’utente aggiorna la pagina, l’errore non venga mostrato. Così potrà inserire nuovamente i propri dati senza visualizzare nessun messaggio d’errore.

Prima di proseguire ed esaminare il codice per le richieste di tipo POST, inviate sia per effettuare il login che per creare un nuovo profilo, riportiamo il codice dei due middleware usati.

// file: middlewares/configAuthMiddleware.js

const login = require('../helpers/login');

function configErrorAuthMiddleware (page, errorMessage) {
  return function errorLoginMiddleware (error, req, res, next) {
    console.log(error);
    const target = res.locals.error || {};
    Object.assign(target,
      {
        [page]: {
          msg: errorMessage || error.message,
          isErrorValid: true
        }
      });
    res.locals.error = target;

    res.status(400).render(page);
  };
}

function configAuthMiddleware (redirect) {
  return function authMiddleware (req, res, next) {
    req.app.locals.error = {};
    req.app.locals.user = req.user.username;
    login(req)
      .then(user => res.redirect(redirect))
      .catch(reason => next(reason));
  };
}

module.exports = {configErrorAuthMiddleware, configAuthMiddleware};

Analizziamo prima il processo di registrazione. Quando un utente invia i suoi dati, usiamo la funzione registerPOST del controller per recuperare le informazioni dell’utente dall’oggetto req.body. Proviamo quindi a creare un nuovo utente. Se già esiste un utente con lo stesso username, passiamo un messaggio di errore al middleware errorLoginMiddleware. Si tratta del middleware restituito dalla funzione configErrorAuthMiddleware. Invece, se l’utente viene creato correttamente, vengono aggiunte le sue informazioni all’oggetto req.user. In questo modo possiamo usare i dati dell’utente nel middleware authMiddleware restituito dalla funzione configAuthMiddleware. Nel middleware authMiddleware effettiamo il login per l’utente usando la funzione login definita nel modulo helpers/login.js.

// file: helpers/login.js

function login (req) {
  return new Promise((resolve, reject) => {
    req.session.regenerate((error) => {
      if (error) {
        reject(error);
      } else {
        console.log('regenerating session...');
        req.session.userID = req.user._id;
        resolve(req.user);
      }
    });
  });
}

module.exports = login;

La funzione login() restituisce una Promise. Dopo aver rigenerato la sessione, se non si verificano errori, aggiungiamo la proprietà userID all’oggetto req.session e risolviamo la promise con l’oggetto req.user.

Ritornando al middleware authMiddleware (funzione restituita da configAuthMiddleware()), se la Promise restituita dalla funzione login() viene risolta con esito positivo, l’utente ha completato la procedura di login e lo redirigiamo alla pagina iniziale.

Nel caso di richieste di tipo POST per il percorso /utenti/login, la funzione loginPOST definita nel file controller/usersRoutesController.js provvede a estrarre i dati dall’oggetto req.body e invoca la funzione User.authenticate definita nel modulo model/user.js. Questa funzione, se ricordate, restituiva un oggetto. La funzione loginPOST estrae le informazioni da tale oggetto. Se l’username e la password inseriti dall’utente coincidono con i dati presenti in un documento della collezione users del database football, la promise hashMatch restituirà ad un certo punto un valore pari a true. In questo caso procediamo come abbiamo già fatto in fase di registrazione. Creiamo infatti un oggetto req.user con le informazioni dell’utente e passiamo la richiesta al middleware authMiddleware che completa la procedura di login.

Conclusioni

In questa lezione abbiamo visto in maniera rapida un esempio completo di applicazione realizzata con Express con diverse funzionalità. Nella prossima lezione vedremo come usare la tecnologia dei WebSocket per realizzare applicazioni Real-time.

Pubblicitร