Objectives

Introduce Sessions onto the Playlist application, enabling user accounts and cookie-based authentication.

Exercises Solutions

If you want to download a complete version of the app as it should be at the end of this lab, then create a new Glitch project, and import wit-ict-summer-school-2017/playlist-3.

Exercises

If you want to download a complete version of the app as it should be at the end of this lab, then create a new Glitch project, and import wit-ict-summer-school-2017/playlist-3.

Exercise 1: Display Durations

Make sure the duration of each song and playlist is visible on the UX

views/partials/listsongs.hbs

<table class="ui table">
  <thead>
    <tr>
      <th>Song</th>
      <th>Artist</th>
      <th>Duration</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    {{#each playlist.songs}}
      <tr>
        <td>
          {{title}}
        </td>
        <td>
          {{artist}}
        </td>
        <td>
          {{duration}}
        </td>
        <td>
          <a href="/playlist/{{../playlist.id}}/deletesong/{{id}}" class="ui icon button">
            <i class="icon trash"></i>
          </a>
        </td>
      </tr>
    {{/each}}
  </tbody>
</table>

Exercise 2: Add Song Duration

Modify the add song form to accept a song duration as well as title and artist:

views/partials/addsong.hbs

<form class="ui stacked segment form" action="/playlist/{{playlist.id}}/addsong" method="POST">
  <div class="two fields">
    <div class="field">
      <label>Title</label>
      <input placeholder="Title" type="text" name="title">
    </div>
    <div class="field">
      <label>Artist</label>
      <input placeholder="Artist" type="text" name="artist">
    </div>
    <div class="field">
      <label>Duration</label>
      <input placeholder="00" type="number" name="duration">
    </div>
  </div>
  <button class="ui blue submit button">Add Song</button>
</form>

controllers/playlist.js

    const newSong = {
      id: uuid(),
      title: request.body.title,
      artist: request.body.artist,
      duration: Number(request.body.duration),
    };

Exercise 3: Active Duration

The durations in each playlist are just constants stored in the database. Currently, if you add a new playlist + songs, then the duration in the playlist will be undefined.

Change this now, such that when you add a song to a playlist the duration displayed on the dashboard for each playlist will be the actual sum of the donations for all songs in the playlist.

...
  addSong(id, song) {
    const playlist = this.getPlaylist(id);
    playlist.songs.push(song);

    let duration = 0;
    for (let i = 0; i < playlist.songs.length; i++) {
      duration += playlist.songs[i].duration;
    }

    playlist.duration = duration;
    this.store.save();
  },
...

Playlist Model

This lab is based on Playlist-3. This can be imported into a new gomix project from /wit-ict-summer-school-2017/playlist-3.

models/playlist-store.js

If your project has any playlists in the json store, delete them now. Your store should look like this:

models/playlist-store.json

{
  "playlistCollection": [
  ]
}

We need a new method in the playlist-store module, which will retrieve a playlist based on a specific user id.

models/playlist-store.js

...
  getUserPlaylists(userid) {
    return this.store.findBy(this.collection, { userid: userid });
  },
...

This new methods takes a userid, and will only fetch playlists belonging to the user with the specific id. We havent introduced the User model yet - nor are we storing playlists with such an id. We will make these changes in this lab.

UserStore

Now we can introduce a new model:

models/user-store.js

'use strict';

const _ = require('lodash');
const JsonStore = require('./json-store');

const userStore = {

  store: new JsonStore('./models/user-store.json', { users: [] }),
  collection: 'users',

  getAllUsers() {
    return this.store.findAll(this.collection);
  },

  addUser(user) {
    this.store.add(this.collection, user);
  },

  getUserById(id) {
    return this.store.findOneBy(this.collection, { id: id });
  },

  getUserByEmail(email) {
    return this.store.findOneBy(this.collection, { email: email });
  },
};

module.exports = userStore;

And this is a pre-populated model store: us

models/user-store.json

{
  "users": [
    {
      "firstName": "homer",
      "lastName": "simpson",
      "email": "homer@simpson.com",
      "password": "secret",
      "id": "3ad52697-6d98-4d80-8273-084de55a86c0"
    },
    {
      "firstName": "marge",
      "lastName": "simpson",
      "email": "marge@simpson.com",
      "password": "secret",
      "id": "2b6f0989-7b7f-4a38-ad26-aa06b922d751"
    }
  ]
}

In the above we are pre-loading two users for test purposes.

Accounts views

We need a suite of new views to support signup / login

views/index.hbs

{{> welcomemenu }}

<section class="ui center aligned middle aligned segment">
  <p> Sign up or Log in... </p>
</section>

views/login.hbs

{{> welcomemenu id='login' }}

<form class="ui stacked segment form" action="/authenticate" method="POST">
  <h3 class="ui header">Log-in</h3>
  <div class="field">
    <label>Email</label> <input placeholder="Email" name="email">
  </div>
  <div class="field">
    <label>Password</label> <input type="password"  name="password">
  </div>
  <button class="ui blue submit button">Login</button>
</form>

views/signup.hbs

{{> welcomemenu id="signup"}}

<form class="ui stacked segment form" action="/register" method="POST">
  <h3 class="ui header">Register</h3>
  <div class="two fields">
    <div class="field">
      <label>First Name</label>
      <input placeholder="First Name" type="text"  name="firstName">
    </div>
    <div class="field">
      <label>Last Name</label>
      <input placeholder="Last Name" type="text"  name="lastName">
    </div>
  </div>
  <div class="field">
    <label>Email</label>
    <input placeholder="Email" type="text" name="email">
  </div>
  <div class="field">
    <label>Password</label>
    <input type="password" name="password">
  </div>
  <button class="ui blue submit button">Submit</button>
</form>

In addition, a new menu which will support the above views:

views/partials/welcomemenu.hbs

<nav class="ui menu">
  <header class="ui header item"> <a href="/"> Playlist 4 </a></header>
  <div class="right menu">
    <a id="signup" class="item" href="/signup"> Signup  </a>
    <a id="login" class="item" href="/login">  Login   </a>
  </div>
</nav>

<script>
  $("#{{id}}").addClass("active item");
</script>

And finally, we need to extend the menu partial - which will introduce a new menu option to allow a user to log out:

views/partials/menu.hbs

<nav class="ui menu">
  <header class="ui header item"> <a href="/"> Playlist 4 </a></header>
  <div class="right menu">
    <a id="dashboard" class="item" href="/dashboard"> Dashboard  </a>
    <a id="about" class="item" href="/about"> About </a>
    <a id="logout" class="item" href="/logout"> Logout </a>
  </div>
</nav>

<script>
  $("#{{id}}").addClass("active item");
</script>

accounts.# Accounts controller

This is a new controller to support these views:

controllers/accounts

'use strict';

const userstore = require('../models/user-store');
const logger = require('../utils/logger');
const uuid = require('uuid');

const accounts = {

  index(request, response) {
    const viewData = {
      title: 'Login or Signup',
    };
    response.render('index', viewData);
  },

  login(request, response) {
    const viewData = {
      title: 'Login to the Service',
    };
    response.render('login', viewData);
  },

  logout(request, response) {
    response.cookie('playlist', '');
    response.redirect('/');
  },

  signup(request, response) {
    const viewData = {
      title: 'Login to the Service',
    };
    response.render('signup', viewData);
  },

  register(request, response) {
    const user = request.body;
    user.id = uuid();
    userstore.addUser(user);
    logger.info(`registering ${user.email}`);
    response.redirect('/');
  },

  authenticate(request, response) {
    const user = userstore.getUserByEmail(request.body.email);
    if (user) {
      response.cookie('playlist', user.email);
      logger.info(`logging in ${user.email}`);
      response.redirect('/dashboard');
    } else {
      response.redirect('/login');
    }
  },

  getCurrentUser(request) {
    const userEmail = request.cookies.playlist;
    return userstore.getUserByEmail(userEmail);
  },
};

module.exports = accounts;

Routes

These new views+ controller all require new routes:

routes.js

In particular, these specific routes:


...
const accounts = require('./controllers/accounts.js');
...

...
router.get('/', accounts.index);
router.get('/login', accounts.login);
router.get('/signup', accounts.signup);
router.get('/logout', accounts.logout);
router.post('/register', accounts.register);
router.post('/authenticate', accounts.authenticate);
...

This is the complete revised routes.js

'use strict';

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

const dashboard = require('./controllers/dashboard.js');
const about = require('./controllers/about.js');
const playlist = require('./controllers/playlist.js');

router.get('/', accounts.index);
router.get('/login', accounts.login);
router.get('/signup', accounts.signup);
router.get('/logout', accounts.logout);
router.post('/register', accounts.register);
router.post('/authenticate', accounts.authenticate);

router.get('/dashboard', dashboard.index);
router.get('/dashboard/deleteplaylist/:id', dashboard.deletePlaylist);
router.post('/dashboard/addplaylist', dashboard.addPlaylist);

router.get('/about', about.index);
router.get('/playlist/:id', playlist.index);
router.get('/playlist/:id/deletesong/:songid', playlist.deleteSong);
router.post('/playlist/:id/addsong', playlist.addSong);

module.exports = router;

The application should be running now. You should see these new views:

A new landing page

Signup

login

Dashboard

Try the following now:

  • Log in as 'homer@simpson', 'secret'.
  • Add a playlist called 'test'.
  • logout
  • Log in as 'marge@simpson', 'secret'.

Notice that we are seeing Homers test playlist even when we log in as marge? The playlist-store.json may look like this:

{
  "playlistCollection": [
    {
      "id": "4a1ea4ec-303e-4b13-bd98-a6b04877e093",
      "title": "test",
      "songs": []
    }
  ]
}

(you will need to refresh the Glitch Editor view to see the above change)

This clearly is not what we were aiming for. We should only present the users own play lists.

Here is how do it - all changes to the dashboard module:

controller/dashboard.js

In the top of the module, import the new accounts module:

...
const accounts = require ('./accounts.js');
...

Revised index action:

...
  index(request, response) {
    logger.info('dashboard rendering');
    const loggedInUser = accounts.getCurrentUser(request);
    const viewData = {
      title: 'Playlist Dashboard',
      playlists: playlistStore.getUserPlaylists(loggedInUser.id),
    };
    logger.info('about to render', playlistStore.getAllPlaylists());
    response.render('dashboard', viewData);
  },
...

Revised addPlaylist action:

...
  addPlaylist(request, response) {
    const loggedInUser = accounts.getCurrentUser(request);
    const newPlayList = {
      id: uuid(),
      userid: loggedInUser.id,
      title: request.body.title,
      songs: [],
    };
    logger.debug('Creating a new Playlist', newPlayList);
    playlistStore.addPlaylist(newPlayList);
    response.redirect('/dashboard');
  },
...

Log in again as marge and homer in turn, creating a single playlist (use a name you will remember). Make sure that the appropriate playlist appears in each users dashboard.

The Stores

Looking at the playlist-store.json - it might (eventually when loaded) look like this:

{
  "playlistCollection": [
    {
      "id": "4a1ea4ec-303e-4b13-bd98-a6b04877e093",
      "title": "test",
      "songs": []
    },
    {
      "id": "1e6ed5a0-28fe-4527-8ce8-6cb5c800b5be",
      "userid": "2b6f0989-7b7f-4a38-ad26-aa06b922d751",
      "title": "marges playlist",
      "songs": []
    },
    {
      "id": "07dd66fe-9f8f-456c-944d-48330bde4610",
      "userid": "3ad52697-6d98-4d80-8273-084de55a86c0",
      "title": "homers playlist",
      "songs": []
    }
  ]
}

Examine it carefully - notice that the very first playlist is 'orphaned' - it has no userid. The others have userid - which corellates the playlists with the user in the users store:

{
  "users": [
    {
      "firstName": "homer",
      "lastName": "simpson",
      "email": "homer@simpson.com",
      "password": "secret",
      "id": "3ad52697-6d98-4d80-8273-084de55a86c0"
    },
    {
      "firstName": "marge",
      "lastName": "simpson",
      "email": "marge@simpson.com",
      "password": "secret",
      "id": "2b6f0989-7b7f-4a38-ad26-aa06b922d751"
    }
  ]
}

Exercise

This is the completed archive at this stage:

It can be imported into gomix using wit-ict-summer-school-2017/playlist-4

Exercise 1

Test the application more comprehensively - signing up a range of users, and creating playlists. Make sure the users only see the playlists they have created.

Exercise 2

Look at the authenticate method again:

  authenticate(request, response) {
    const user = userstore.getUserByEmail(request.body.email);
    if (user) {
      response.cookie('playlist', user.email);
      logger.info(`logging in ${user.email}`);
      response.redirect('/dashboard');
    } else {
      response.redirect('/login');
    }
  },

Can you see anything not quite right about it?

Hint: what happens if incorrect password entered? Try this now.

See if you can fix this problem - i.e. only allow user to log in if they provide correct password.