├── .gitignore ├── api ├── Games.js ├── Players.js ├── Sessions.js ├── Turns.js ├── checks.js └── models.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .monitor 3 | -------------------------------------------------------------------------------- /api/Games.js: -------------------------------------------------------------------------------- 1 | var models = require( './models.js' ); 2 | var checks = require( './checks.js' ); 3 | 4 | exports.bindToApp = function( app ) { 5 | app.post( '/api/1.0/Game', checks.player, function( request, response ) { 6 | 7 | var game = new models.Game(); 8 | game.gameType = request.param( 'gameType' ); 9 | game.initialState = request.param( 'initialState' ); 10 | game.maxPlayers = request.param( 'maxPlayers' ) ? request.param( 'maxPlayers' ) : 2; 11 | game.players = [ request.session.player.uuid ]; 12 | 13 | game.save( function( error ) { 14 | if ( error ) 15 | { 16 | response.json( error.message ? error.message : error, 500 ); 17 | return; 18 | } 19 | 20 | response.json( game ); 21 | }); 22 | }); 23 | 24 | app.get( '/api/1.0/Game/:gameId', function( request, response ) { 25 | models.Game.findById( request.params.gameId, function( error, game ) { 26 | if ( error ) 27 | { 28 | response.json( error, 500 ); 29 | return; 30 | } 31 | 32 | if ( !game ) 33 | { 34 | response.json( 'No game found with id: ' + request.params.gameId, 404 ); 35 | return; 36 | } 37 | 38 | response.json( game ); 39 | }); 40 | }); 41 | 42 | app.get( '/api/1.0/Games', checks.player, function( request, response ) { 43 | models.Game.find( { 'players': request.session.player.uuid }, function( error, games ) { 44 | if ( error ) 45 | { 46 | response.json( error, 500 ); 47 | return; 48 | } 49 | 50 | response.json( games ); 51 | }); 52 | }); 53 | 54 | app.put( '/api/1.0/Game/:gameId/Players/:playerUUID', function( request, response ) { 55 | models.Game.findById( request.params.gameId, function( error, game ) { 56 | if ( error ) 57 | { 58 | response.json( error ); 59 | return; 60 | } 61 | 62 | if ( !game ) 63 | { 64 | response.json( "Cloud not locate a game with id: " + request.params.gameId, 404 ); 65 | return; 66 | } 67 | 68 | if ( game.maxPlayers == game.players.length ) 69 | { 70 | response.json( "This game is already full.", 400 ); 71 | return; 72 | } 73 | 74 | game.players.push( request.params.playerUUID ); 75 | game.save( function( error ) { 76 | if ( error ) 77 | { 78 | response.json( error, 500 ); 79 | return; 80 | } 81 | 82 | response.json( game ); 83 | }); 84 | }); 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /api/Players.js: -------------------------------------------------------------------------------- 1 | var models = require( './models.js' ); 2 | var checks = require( './checks.js' ); 3 | 4 | var uuid = require( 'node-uuid' ); 5 | var sha1 = require( 'sha1' ); 6 | 7 | exports.bindToApp = function( app ) { 8 | app.post( '/api/1.0/Player', function( request, response ) { 9 | 10 | if ( !request.param( 'username' ) ) 11 | { 12 | response.json( 'You must specify a username to sign up!', 400 ); 13 | return; 14 | } 15 | 16 | models.Player.findOne( { 'username': request.param( 'username' ).toLowerCase() }, function( error, player ) { 17 | if ( error ) 18 | { 19 | response.json( error, 500 ); 20 | return; 21 | } 22 | 23 | if ( player ) 24 | { 25 | response.json( 'A player with username "' + request.param( 'username' ) + '" already exists.', 403 ); 26 | return; 27 | } 28 | 29 | player = new models.Player(); 30 | player.username = request.param( 'username' ).toLowerCase(); 31 | player.uuid = uuid.v4(); 32 | player.passwordHash = request.param( 'password' ) ? sha1( request.param( 'password' ) ) : null; 33 | 34 | player.save( function( error ) { 35 | if ( error ) 36 | { 37 | response.json( error, 500 ); 38 | return; 39 | } 40 | 41 | request.session.player = player; 42 | request.session.save(); 43 | 44 | response.json( models.censor( player, { 'passwordHash': true } ) ); 45 | }); 46 | }); 47 | }); 48 | 49 | app.put( '/api/1.0/Player', checks.player, function( request, response ) { 50 | 51 | function save() 52 | { 53 | models.Player.findById( request.session.player._id, function( error, player ) { 54 | player.username = typeof( request.params.username ) != undefined ? request.params.username.toLowerCase() : player.username; 55 | player.passwordHash = typeof( request.params.password ) != undefined ? sha1( request.params.password ) : player.passwordHash; 56 | 57 | player.save( function( error ) { 58 | if ( error ) 59 | { 60 | response.json( error, 500 ); 61 | return; 62 | } 63 | 64 | request.session.player = player; 65 | request.session.save(); 66 | 67 | response.json( models.censor( player, { 'passwordHash': true } ) ); 68 | }); 69 | }); 70 | } 71 | 72 | if ( request.params.username ) 73 | { 74 | models.User.findOne( { 'username': request.params.username.toLowerCase() }, function( error, player ) { 75 | if ( error ) 76 | { 77 | response.json( error, 500 ); 78 | return; 79 | } 80 | 81 | if ( player ) 82 | { 83 | response.json( 'A player already exists with that username.', 409 ); 84 | return; 85 | } 86 | 87 | save(); 88 | }); 89 | } 90 | else 91 | { 92 | save(); 93 | } 94 | }); 95 | 96 | app.get( '/api/1.0/Player', checks.player, function( request, response ) { 97 | response.json( models.censor( request.session.player, { 'passwordHash': true } ) ); 98 | }); 99 | 100 | app.get( '/api/1.0/Player/:username', function( request, response ) { 101 | models.Player.findOne( { 'username': request.params.username.toLowerCase() }, function( error, player ) { 102 | if ( error ) 103 | { 104 | response.json( error, 500 ); 105 | return; 106 | } 107 | 108 | if ( !player ) 109 | { 110 | response.json( 'No player found with username: ' + request.params.username, 404 ); 111 | return; 112 | } 113 | 114 | response.json( models.censor( player, { 'passwordHash': true } ) ); 115 | }); 116 | }); 117 | } -------------------------------------------------------------------------------- /api/Sessions.js: -------------------------------------------------------------------------------- 1 | var models = require( './models.js' ); 2 | var checks = require( './checks.js' ); 3 | 4 | var sha1 = require( 'sha1' ); 5 | 6 | exports.bindToApp = function( app ) { 7 | 8 | app.post( '/api/1.0/Session', checks.player, function( request, response ) { 9 | response.json( { 'created': true } ); 10 | }); 11 | 12 | app.del( '/api/1.0/Session', checks.player, function( request, response ) { 13 | if ( !request.session ) 14 | { 15 | response.json( 'No current session.', 404 ); 16 | return; 17 | } 18 | 19 | request.session.destroy( function() { 20 | response.json( { 'removed': true } ); 21 | }); 22 | }); 23 | } -------------------------------------------------------------------------------- /api/Turns.js: -------------------------------------------------------------------------------- 1 | var models = require( './models.js' ); 2 | var checks = require( './checks.js' ); 3 | 4 | exports.bindToApp = function( app ) { 5 | app.get( '/api/1.0/Game/:gameId/Turns', function( request, response ) { 6 | models.Game.findById( request.params.gameId, function( error, game ) { 7 | if ( error ) 8 | { 9 | response.json( error, 500 ); 10 | return; 11 | } 12 | 13 | if ( !game ) 14 | { 15 | response.json( "No game with id: " + request.params.gameId, 404 ); 16 | return; 17 | } 18 | 19 | models.Turn.find( { 'gameId': game._id }, function( error, turns ) { 20 | if ( error ) 21 | { 22 | response.json( error, 500 ); 23 | return; 24 | } 25 | 26 | turns.sort( function( lhs, rhs ) { 27 | return lhs.createdAt < rhs.createdAt; 28 | }); 29 | 30 | response.json( turns ); 31 | }); 32 | }); 33 | }); 34 | 35 | app.get( '/api/1.0/Game/:gameId/WhoseTurnIsIt', function( request, response ) { 36 | models.Game.findById( request.params.gameId, function( error, game ) { 37 | if ( error ) 38 | { 39 | response.json( error, 500 ); 40 | return; 41 | } 42 | 43 | if ( !game ) 44 | { 45 | response.json( "No game with id: " + request.params.gameId, 404 ); 46 | return; 47 | } 48 | 49 | models.Turn.find().where( 'gameId', game._id ).desc( 'createdAt' ).limit( 1 ).exec( function( error, turns ) { 50 | if ( error ) 51 | { 52 | response.json( error, 500 ); 53 | return; 54 | } 55 | 56 | if ( turns.length == 0 ) 57 | { 58 | response.json( game.players[ 0 ] ); 59 | return; 60 | } 61 | 62 | var turn = turns[ 0 ]; 63 | 64 | for ( var playerIndex = 0; playerIndex < game.players.length; ++playerIndex ) 65 | { 66 | if ( game.players[ playerIndex ] == turn.playerUUID ) 67 | { 68 | response.json( game.players[ ( playerIndex + 1 ) % game.players.length ] ); 69 | return; 70 | } 71 | } 72 | }); 73 | }); 74 | }); 75 | 76 | app.post( '/api/1.0/Game/:gameId/Turn', checks.player, function( request, response ) { 77 | models.Game.findById( request.params.gameId, function( error, game ) { 78 | if ( error ) 79 | { 80 | response.json( error, 500 ); 81 | return; 82 | } 83 | 84 | if ( !game ) 85 | { 86 | response.json( "No game found with id: " + request.params.gameId, 404 ); 87 | return; 88 | } 89 | 90 | var newTurn = new models.Turn(); 91 | newTurn.gameId = game._id; 92 | newTurn.playerUUID = request.session.player.uuid; 93 | newTurn.data = request.params.data; 94 | 95 | newTurn.save( function( error ) { 96 | if ( error ) 97 | { 98 | response.json( error, 500 ); 99 | return; 100 | } 101 | 102 | response.json( newTurn ); 103 | }); 104 | }); 105 | }); 106 | }; 107 | 108 | -------------------------------------------------------------------------------- /api/checks.js: -------------------------------------------------------------------------------- 1 | var models = require( './models.js' ); 2 | 3 | var sha1 = require( 'sha1' ); 4 | 5 | exports.player = function( request, response, next ) 6 | { 7 | if ( request.session.player ) 8 | { 9 | next(); 10 | return; 11 | } 12 | 13 | var username = null; 14 | var password = null; 15 | 16 | var authorization = request.headers.authorization; 17 | if ( authorization ) 18 | { 19 | var parts = authorization.split(' '); 20 | var scheme = parts[0]; 21 | var credentials = new Buffer( parts[ 1 ], 'base64' ).toString().split( ':' ); 22 | 23 | if ( 'Basic' != scheme ) 24 | { 25 | response.send( 'Basic authorization is the only supported authorization scheme.', 400 ); 26 | return; 27 | } 28 | 29 | username = credentials[ 0 ]; 30 | password = credentials[ 1 ]; 31 | } 32 | else 33 | { 34 | username = request.params.username; 35 | password = request.params.password; 36 | } 37 | 38 | if ( !username ) 39 | { 40 | response.json( "You must specify a username for authentication.", 400 ); 41 | return; 42 | } 43 | 44 | models.Player.findOne( { 'username': username.toLowerCase() }, function( error, player ) { 45 | if ( error ) 46 | { 47 | response.json( error, 500 ); 48 | return; 49 | } 50 | 51 | if ( !player ) 52 | { 53 | response.json( 'Could not locate a player with username: ' + username, 404 ); 54 | return; 55 | } 56 | 57 | if ( player.passwordHash && player.passwordHash != sha1( password ) ) 58 | { 59 | response.json( 'Invalid password.', 403 ); 60 | return; 61 | } 62 | 63 | request.session.player = player; 64 | request.session.save(); 65 | next(); 66 | return; 67 | }); 68 | } 69 | 70 | exports.ownsContext = function( request, response, next ) { 71 | 72 | if ( !request.session.user ) 73 | { 74 | response.json( 'Server error: user session does not exist. Please report this problem.', 500 ); 75 | return; 76 | } 77 | 78 | models.Context.findById( request.params.contextId, function( error, context ) { 79 | if ( error ) 80 | { 81 | response.json( error, 500 ); 82 | return; 83 | } 84 | 85 | if ( !context ) 86 | { 87 | response.json( 'No context for id: ' + request.params.contextId, 404 ); 88 | return; 89 | } 90 | 91 | if ( context.owners.indexOf( request.session.user.hash ) == -1 ) 92 | { 93 | response.json( 'You are not authorized to access this resource.', 403 ); 94 | return; 95 | } 96 | 97 | request.context = context; 98 | next(); 99 | }); 100 | } 101 | 102 | exports.ownsAchievementClass = function( request, response, next ) { 103 | if ( !request.session.user ) 104 | { 105 | response.json( 'Server programming error: user session does not exist when checking achievement class ownership. Please report this problem.', 500 ); 106 | return; 107 | } 108 | 109 | if ( !request.context ) 110 | { 111 | response.json( 'Server programming error: context is not set when checking achievement class ownership. Please report this problem.', 500 ); 112 | return; 113 | } 114 | 115 | models.AchievementClass.findById( request.params.achievementClassId, function( error, achievementClass ) { 116 | if ( error ) 117 | { 118 | response.json( error, 500 ); 119 | return; 120 | } 121 | 122 | if ( !achievementClass ) 123 | { 124 | response.json( 'No achievement class for id: ' + request.params.achievementClassId, 404 ); 125 | return; 126 | } 127 | 128 | if ( !achievementClass.contextId.equals( request.context._id ) ) 129 | { 130 | response.json( 'Context with id ' + request.context._id + ' does not own achievement class with id ' + achievementClass._id, 403 ); 131 | return; 132 | } 133 | 134 | request.achievementClass = achievementClass; 135 | next(); 136 | }); 137 | } -------------------------------------------------------------------------------- /api/models.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ); 2 | var MongooseTypes = require( 'mongoose-types' ); 3 | MongooseTypes.loadTypes( mongoose ); 4 | var UseTimestamps = MongooseTypes.useTimestamps; 5 | 6 | // TODO: make this be on the mongoose model prototype 7 | var censor = exports.censor = function ( object, fields ) 8 | { 9 | var censored = {}; 10 | for ( var key in ( object._doc || object ) ) 11 | { 12 | if ( !( key in fields ) ) 13 | { 14 | censored[ key ] = object[ key ]; 15 | } 16 | } 17 | return censored; 18 | } 19 | 20 | exports.PlayerSchema = new mongoose.Schema({ 21 | uuid: { type: String, unique: true, index: true }, 22 | username: { type: String, unique: true, index: true }, 23 | passwordHash: { type: String } 24 | }); 25 | exports.PlayerSchema.plugin( UseTimestamps ); 26 | exports.Player = mongoose.model( 'Player', exports.PlayerSchema ); 27 | 28 | exports.GameSchema = new mongoose.Schema({ 29 | gameType: { type: String, index: true }, 30 | initialState: { type: String }, 31 | maxPlayers: { type: Number }, 32 | players: { type: Array, index: true }, 33 | completed: { type: Date } 34 | }); 35 | exports.GameSchema.plugin( UseTimestamps ); 36 | exports.Game = mongoose.model( 'Game', exports.GameSchema ); 37 | 38 | exports.TurnSchema = new mongoose.Schema({ 39 | gameId: { type: mongoose.Schema.ObjectId, index: true }, 40 | playerUUID: { type: mongoose.Schema.ObjectId, index: true }, 41 | data: { type: String } 42 | }); 43 | exports.TurnSchema.plugin( UseTimestamps ); 44 | exports.Turn = mongoose.model( 'Turn', exports.TurnSchema ); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TurnBased", 3 | "description": "Turn-based-games for the web!", 4 | "author": "Andy Burke ", 5 | "version": "0.0.1", 6 | "dependencies": { 7 | "node-uuid": "1.3.x", 8 | "mongodb": "0.9.x", 9 | "mongoose": "2.4.x", 10 | "mongoose-types": "1.0.x", 11 | "connect-mongodb": "1.1.x", 12 | "sha1": "0.0.x", 13 | "MD5": "0.0.x", 14 | "express": "2.5.x" 15 | }, 16 | "devDependencies": { 17 | "jasmine-node": "1.0.x", 18 | "shred": "https://github.com/andyburke/shred/tarball/master" 19 | }, 20 | "engine": "node >= 0.6.6" 21 | } 22 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require( 'express' ); 2 | var sha1 = require( 'sha1' ); 3 | 4 | var dbHost = process.env[ 'MONGO_HOST' ] != null ? process.env[ 'MONGO_HOST' ] : 'localhost'; 5 | var dbPort = process.env[ 'MONGO_PORT' ] != null ? process.env[ 'MONGO_PORT' ] : 27017; 6 | var dbName = process.env[ 'TURNS_DB' ] != null ? process.env[ 'TURNS_DB' ] : 'turns'; 7 | 8 | var mongoose = require( 'mongoose' ); 9 | mongoose.connect( dbHost, dbName, dbPort ); 10 | 11 | var mongo = require( 'mongodb' ); 12 | var sessionDb = new mongo.Db( dbName, new mongo.Server( dbHost, dbPort, { auto_reconnect: true } ), {} ); 13 | var mongoStore = require( 'connect-mongodb' ); 14 | 15 | var sessionSecret = process.env[ 'TURNS_SECRET' ] != null ? sha1( process.env[ 'TURNS_SECRET' ] ) : sha1( __dirname + __filename + process.env[ 'USER' ] ); 16 | 17 | var app = express.createServer( 18 | express.static( __dirname + '/site' ), 19 | express.bodyParser(), 20 | express.cookieParser(), 21 | express.session({ 22 | cookie: { maxAge: 60000 * 60 * 24 * 30 }, // 30 days 23 | secret: sessionSecret, 24 | store: new mongoStore( { db: sessionDb } ) 25 | }) 26 | ); 27 | 28 | var apiModules = [ 29 | require( './api/Sessions.js' ), 30 | require( './api/Players.js' ), 31 | require( './api/Games.js' ), 32 | require( './api/Turns.js' ) 33 | ]; 34 | 35 | for ( var moduleIndex = 0; moduleIndex < apiModules.length; ++moduleIndex ) 36 | { 37 | apiModules[ moduleIndex ].bindToApp( app ); 38 | } 39 | 40 | app.listen( process.env.TURNS_PORT || 8000 ); 41 | --------------------------------------------------------------------------------