WebFund 2014W: Assignment 2

From Soma-notes

The solutions to this assignment are now available.

In this assignment you are to examine, modify, and answer questions regarding the adventure-demo (without modules) sample node application. The files of this application are also listed below. Note that to run this application you'll first need to run node storeRooms.js to setup the rooms collection in MongoDB.

Please submit your answers as a single zip file called "<username>-comp2406-assign2.zip" (where username is your MyCarletonOne username). This zip file should unpack into a directory of the same name (minus the .zip extension of course). This directory should contain:

  • your modified version of adventure-demo (one version with all code changes) and
  • a text file called "answers.txt" or "answers.pdf" at the top level (in text or PDF format, respectively) that contains the answers to the all of the questions, with the first four lines being "COMP 2406 Assignment 2", your name, student number, and the date. For questions requiring code changes (Part B), explain in your answers file which parts of the program were changed. You may wish to format answers.txt in Markdown to improve its appearance.

No other formats will be accepted. Submitting in another format will likely result in your assignment not being graded and you receiving no marks for this assignment. In particular do not submit an MS Word or OpenOffice file as your answers document!

Questions

There are 50 points below in two parts. Answer all questions.

Part A

Describe how to change the code to add the following features/change the following behaviour. If the changes are small, specify the line(s) to changed. If the changes are more substantial, you may just list the entire modified file. These changes are cumulative, so the changes for the third question should take into account those made previously.

  1. (2 points) Make the secret room accessible by visiting "/secret".
  2. (4 points) Add a room to the game that is connected to the others so you can enter and exit it as you can the bridge, sickbay, and engineering.
  3. (4 points) Add a check to make sure that the player's name only contains alphanumeric characters.
  4. (10 points) Modify the start page (/) so that it lists the other players that are currently playing at the bottom, saying "Currently playing:" on one line and then a comma-separated list of players on the next. Note that this list should just contain the players with active sessions, not those who have created accounts. If no players are currently playing this part of the page should be omitted. Like other data stored by this application, this information should be persistent across server restarts.

Part B

  1. (4 points) What is the key difference between the Start and Register button on the initial screen? Do they work the same way?
  2. (4 points) MongoDB's "tables" are collections; they are grouped together into databases. What MongoDB database is used by this application? What collections?
  3. (2 points) How long before this app's session cookies expire? How do you know?
  4. (4 points) Do sessions and user accounts persist across web application restarts? Why or why not?
  5. (4 points) In the POST function for /start, it processes a username and password supplied by the user. What object stores this information in node for our program to access? What line in app.js loads the code to provides the values for this object?
  6. (4 points) Why are there three arguments to the app.get()'s, rather than the previous two? What does the third argument do?
  7. (2 points) What is purpose of the call to playersCollection.update() (line 115 of routes/index.js)?
  8. (6 points) When does makeRoomHandler() run? What does it return? And when is its single argument accessed?

Source

app.js

/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var path = require('path');
var fs = require('fs');

var http = require('https');  // note https, not http!
var MongoStore = require('connect-mongo')(express);

var app = express();

var options = {
  key: fs.readFileSync('keys/comp2406-private-key.pem'),
  cert: fs.readFileSync('keys/comp2406-cert.pem')
};

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());

app.use(express.cookieParser('COMP 2406 adventure demo!'));
app.use(express.session({
    cookie: {maxAge: 60000 * 20} // 20 minutes
    , secret: "Shh... I'm a secret"
    , store: new MongoStore({db: "adventure-demo"})
}));

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

// set NODE_ENV environment variable to change from development mode
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.redirectLoggedIn, routes.index);
app.post("/register", routes.register);
app.post("/start", routes.start);
app.post("/quit", routes.quit);

var createRooms = function() {
    var i, theRoom;

    routes.getRooms().toArray(
	function(err, docs) {
	    if (err) {
		throw "Couldn't find active room list";
	    }
	    
	    var activeRooms = docs[0].activeRooms;

	    activeRooms.forEach(function(roomName) {
		console.log('Creating room: ' + roomName);
		app.get('/' + roomName,
			routes.makeRoomHandler(roomName));
	    });
	}
    );
}
routes.connectToDBs(createRooms);

http.createServer(options, app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port') +
	      ' in ' + app.get('env') + ' mode.');
});


routes/index.js

var bcrypt = require("bcrypt");
var mc = require('mongodb').MongoClient;
var playersCollection;
var roomsCollection;

var connectToDBs = function(callback) {
    mc.connect('mongodb://localhost/adventure-demo', function(err, db) {
	if (err) {
	    throw err;
	}
	
	playersCollection = db.collection('players');
	roomsCollection = db.collection('rooms');

	callback();
    });
}

var index = function(req, res){
    res.render('index', { title: 'COMP 2406 Adventure Demo',
			  error: req.query.error });
}

var redirectLoggedIn = function(req, res, next) {
    if (req.session.player) {
        res.redirect("/" + req.session.player.room);
    } else {
        next();
    }
}

var redirectNotLoggedIn = function(req, res, next) {
    if (req.session.player) {
        next();
    } else {
        res.redirect("/");
    }
}

var register = function(req, res) {
    var playername = req.body.playername;
    var password = req.body.password;

    var addPlayer = function(err, players) {
	if(players.length!=0){
	    res.redirect("/?error=player already exists");	
	    return;
	}
	
	//generate a salt, with 10 rounds (2^10 iterations)
	bcrypt.genSalt(10, function(err, salt) {
	    bcrypt.hash(password, salt, function(err, hash) {
      		var newPlayer = {
      		    playername: playername,
      		    password: hash,
		    room: "bridge"
      		};
      		
      		playersCollection.insert(newPlayer, function(err, newPlayers){
		    if (err) {
			throw err;
		    } else {
      			res.render('registered', 
				   { playername: newPlayers[0].playername });
		    }
      		});    
	    });
	});	
    };	
    
    playersCollection.find({playername: playername}).toArray(addPlayer);
}

var start = function(req, res){
    var playername = req.body.playername;
    var password = req.body.password;

    playersCollection.findOne({playername: playername}, function(err, player){
	
	if (err || !player){
	    req.session.destroy(function(err) {
		res.redirect("/?error=invalid playername or password");	
	    });
	    return;
	}
	
	bcrypt.compare(password, player.password, function(err, authenticated){
	    if(authenticated){
		req.session.player = player;
		delete req.session.player._id;
		res.redirect("/" + player.room);
	    } else {
		req.session.destroy(function(err) {
		    res.redirect("/?error=invalid playername or password");
		});
	    }
	});
    });
}

var quit = function(req, res){
    req.session.destroy(function(err){
	if(err){
            console.log("Error: %s", err);
	}
	res.redirect("/");
    });	
}

var makeRoomHandler = function(roomName) {
    handler = function(req, res) {
	if (req.session.player) {
	    var player = req.session.player;
	    player.room = roomName;
	    playersCollection.update({"playername": player.playername},
				     player, function(err, count) {
					 if (err) {
					     console.log(
						 "Couldn't save player state");
					 }
				     });
	    roomsCollection.findOne(
		{name: roomName},
		function(err, room) {
		    if (err || !room) {
			throw "Couldn't find room " + roomName;
		    }
		    res.render("room.jade", room);
		}
	    );
	} else {
            res.redirect("/");
	}
    }
    return handler;
}

var getRooms = function() {
    return roomsCollection.find({name: "roomList"});
}

exports.index = index;
exports.redirectLoggedIn = redirectLoggedIn;
exports.redirectNotLoggedIn = redirectNotLoggedIn;
exports.register = register;
exports.start = start;
exports.quit = quit;
exports.makeRoomHandler = makeRoomHandler;
exports.getRooms = getRooms;
exports.connectToDBs = connectToDBs;


views/layout.jade

doctype html
html
  head
    title= title
    script(src='/vendor/jquery/jquery.js')
    script('vendor/bootstrap/dist/js/bootstrap.js')
    link(rel='stylesheet', href='/vendor/bootstrap/dist/css/bootstrap.css')
    link(rel='stylesheet', href='/stylesheets/style.css')
    block header  
  body
    block content


views/index.jade

extends layout

block header
  script(src='/javascripts/home.js')

block content
  h1= title
  - if(error)
    div.alert-error #{error}
  p Please log in
  div
    form(action="/start", method="post")
        div.control-group.input-append
            input(type="text", name="playername")
            label.add-on(for="playername") Player Name
        div.control-group.input-append
            input(type="password", name="password")
            label.add-on(for="password") Password
            
        button(type="submit") Start
        button#register(type="button") Register


views/registered.jade

extends layout

block content
  h1 Registration Successful	
  p You have successfully registered player #{playername}
  form(action="/", method="get")
        button(type="submit") Return


views/room.jade

extends layout

block content
  h1= title
  p #{description}
  p Go to:
  ul
    each theExit in roomExits
      li
        a(href= theExit) #{theExit}
  form(action="/quit", method="post")
     button(type="submit") Quit


public/javascripts/home.js

$(function(){
	$("#register").on("click",function(){
		var $form = $("form");
		$form.attr("action","/register");
		$form.submit();
	});
});


storeRooms.js

// storeRooms.js

var mc = require('mongodb').MongoClient;

var rooms = [
    
    {
	name: "roomList",
	activeRooms: ['bridge', 'sickbay', 'engineering']
    },
    {   name: "bridge",
	title: "Bridge",
	description: "You are on the Bridge.  There are big comfy chairs and a big screen here.",
	roomExits: ['sickbay']
    },    
    {
	name: "engineering",
	title: "Engineering",
	description: "You are in Engineering.  There are lots of funny instruments, many smaller screens, and kind of uncomfortable chairs.",
	roomExits: ['sickbay']
    },

    {
	name: "sickbay",
	title: "Sickbay",
	description: "You are in Sickbay.  It is in the center of the ship, the safest place there is.  There are lots of comfy beds here and blinky lights.",
	roomExits: ['engineering','bridge']
    },

    {
	name: "secret",
	title: "Secret Room",
	description: "This is a secret room.  How did you get here?",
	roomExits: ['engineering', 'sickbay', 'bridge']
    }
];

mc.connect('mongodb://localhost/adventure-demo', function(err, db) {
    if (err) {
	throw err;
    }
    
    var roomsCollection = db.collection('rooms');

    roomsCollection.drop(function(err, count) {
	if (err) {
	    console.log("No collection to drop.");
	} else {
	    console.log("room collection dropped.");
	}
	roomsCollection.insert(rooms, function(err, rooms) {
	    if (err) {
		throw err;
	    }

	    rooms.forEach(function(room) {
		console.log("Added " + room.name);
	    });
	    db.close();
	});
    });
});