Merge branch 'master' of github.com:Deployd/Deployd

This commit is contained in:
Jeff Cross
2011-11-26 15:43:35 -07:00
20 changed files with 96 additions and 624 deletions

View File

@@ -55,107 +55,4 @@ app.get('/routes', function(req, res) {
app.get('/', function(req, res) {
res.send({home: true});
});
app.get('/config', function(req, res) {
res.send({
name: 'Hello World',
host: 'foo.bar.com'
})
});
app.get('/models', function(req, res) {
res.send({
results: [{
plugin: 'Users',
name: 'Users',
_id: 5432
}]
})
});
app.get('/plugins', function(req, res) {
res.send({
results: [{
name: 'Users',
overview_html: '<p>The Users plugin lets you configure users...</p>',
configurable_objects: [
{
name: "Roles and Permissions"
, id: 1
},
{
name: "Existing Users"
, id: 2
, list: "User List"
, source: "/users"
},
{
name: "Model",
id: 3,
model_description: {
password:"password",
uid:"email",
removed:"boolean",
name:"string",
auth:"string"
},
plugin: "graph",
_id: "4ec48fa3d1a11cd925000007"
},
{
/*
description: {
uid: 'email',
password: 'password',
name: 'string',
auth: 'string',
removed: 'boolean'
},
*/
name: "New User"
, id: 4
, helper_text: "Fill out the form below to create a new user"
, form: {
action: "/user"
, method: "POST"
, fields: [
{
name: "name"
, label: "Full Name"
, type: "text"
, required: true
},
{
name: "uid"
, label: "Email"
, type: "email"
, required: true
},
{
name: "password"
, label: "Password"
, type: "password"
, required: true
}
]
}
}
],
_id: 1234
},
{
name: "Phone",
overview_html: '<p>The Phone plugin lets you configure phone stuff...</p>',
configurable_objects: [
{
name: "Numbers"
, id: 1
}
],
_id: 2345
}]
})
});

View File

@@ -4,6 +4,8 @@ var Model = require('./model')
module.exports = Model.spawn({
isCollection: true,
initialize: function() {
this.models = [];
},
@@ -47,6 +49,33 @@ module.exports = Model.spawn({
this.models = changes;
this.state = this._states.ready;
this.emit('change:state');
},
updateSettings: function() {
// TODO, implement basic settings
},
defineRoutes: function(app) {
var collection = this.collection
, model = this
, plugin = this.plugin
, base = (plugin === collection) ? '' : ('/' + plugin)
, route = [base, collection].join('/')
;
// one model
app.get(route, function(req, res) {
var query = req.query;
model
.spawn()
.for(req)
.find(query)
.set(req.body)
.notify(res)
.fetch()
;
});
}
});

View File

@@ -30,7 +30,7 @@ module.exports = {
find: ready(function(model) {
var query = model.toQuery() || {}
, id = model.get('_id')
, id = model.get('_id') || (query && query._id)
, _id = id && ObjectID(id)
;

View File

@@ -9,6 +9,7 @@ var Model
, db = require('./db')
, compile = require('./types').compile
, _ = require('underscore')
, app = require('./app')
;
var emitter = new EventEmitter();
@@ -149,6 +150,7 @@ Model = module.exports = emitter.spawn({
, actor = this.actor()
, allowed = true
, model = this
, User = require('./plugins/users/user')
;
if(requiresUser && !actor) {
@@ -159,9 +161,9 @@ Model = module.exports = emitter.spawn({
if(permissions && rights && rights !== 'public') {
// check permission against actor
Model
.spawn({collection: 'users', allowed: false})
.set({_id: actor})
User
.spawn()
.find({_id: actor})
.notify(function(json) {
// TODO cache groups on the req
var groups = json.groups
@@ -207,7 +209,7 @@ Model = module.exports = emitter.spawn({
fn();
},
for: function(req) {
for: function(req) {
if(req.session && req.session.user) {
this.actor(req.session.user._id);
}
@@ -258,7 +260,7 @@ Model = module.exports = emitter.spawn({
set: function(changes) {
var _self = this;
if(!changes) return this;
Object.getOwnPropertyNames(changes).forEach(function(p) {
if(_self.attributes[p] != changes[p]) {
_self.attributes[p] = changes[p];
@@ -347,7 +349,7 @@ Model = module.exports = emitter.spawn({
return this;
},
plugin: 'graph',
plugin: 'models',
updateSettings: function() {
var settings = Model.spawn()
@@ -359,9 +361,8 @@ Model = module.exports = emitter.spawn({
if(this.description) {
settings
.find({name: this.name || this.collection, plugin: this.plugin})
.find({collection: this.collection, plugin: this.plugin})
.set({
name: this.name || this.collection,
description: this.description,
plugin: this.plugin,
collection: this.collection,
@@ -394,12 +395,53 @@ Model = module.exports = emitter.spawn({
}
}
}
},
defineRoutes: function(app) {
var collection = this.collection
, model = this
, plugin = this.plugin
, base = (plugin === collection) ? '' : ('/' + plugin)
, route = [base, collection].join('/')
, idRoute = [route, ':id'].join('/')
, methodMap = {
GET: 'fetch',
POST: 'save',
PUT: 'save',
DELETE: 'remove'
}
;
function handler(req, res) {
var query = req.params
, action = methodMap[req.method] || 'fetch'
;
model
.spawn()
.for(req)
.find(query)
.set(req.body)
.notify(res)
[action]()
;
}
// create
app.post(route, handler);
// read
app.get(idRoute, handler);
// update
app.put(idRoute, handler);
// delete
app.del(idRoute, handler);
}
});
var spawn = module.exports.spawn
, _models = {}
, _collections = {}
;
module.exports.refreshSettings = function(collection) {
@@ -407,10 +449,14 @@ module.exports.refreshSettings = function(collection) {
}
module.exports.spawn = function(model) {
var instance = spawn.apply(this, arguments);
if(model && model.collection) {
_models[instance.collection] = instance;
var instance = spawn.apply(this, arguments)
, cache = instance.isCollection ? _collections : _models;
if(model && model.collection && !cache[instance.collection]) {
cache[instance.collection] = instance;
instance.updateSettings();
instance.defineRoutes(app);
}
return instance;
};

View File

@@ -1,54 +0,0 @@
var Model = require('../../model');
module.exports = Model.spawn({
collection: 'apps',
plugin: 'app',
description: {
name: {type: 'string', unique: true},
creator: 'string',
db: 'string',
host: 'string'
},
allowed: {
read: 'creator',
write: 'creator',
remove: 'creator',
create: 'user'
},
save: function() {
var name = this.get('name')
, host = this.toHostName()
, creator = this.get('creator')
;
if(!creator) this.error('Must be logged in to create an app.', 'App');
if(host && creator) {
if(this.isNew()) {
this.set({name: name, host: host, db: this.toDatabaseName(), creator: creator});
} else {
this.restart(host);
}
}
Model.save.apply(this, arguments);
},
toHostName: function() {
var name = this.get('name')
, app = name && name.replace(/ /g, '-')
, creator = this.get('creator')
;
return app.toLowerCase();
},
toDatabaseName: function() {
return this.toHostName().replace(/\./g, ':').toLowerCase();
}
});

View File

@@ -1,8 +0,0 @@
var Collection = require('../../collection')
, App = require('./app')
;
module.exports = Collection.spawn({
collection: 'apps',
model: App
});

View File

@@ -1,74 +0,0 @@
// Balancer tells requests where to go
// - If a internal host (ihost) is not found within its cache, it boots a deployd
// app at a new ihost and saves this location to the central deployd db
// - Subsequent requests should be mapped from host to ihost and routed
var bouncy = require('bouncy')
, apps = {}
, exec = require('child_process').exec
, ERROR = '<h1>Not Found</h1>There is no app configured to listen to this host. Perhaps you mistyped the url?\n'
, App = require('./app')
;
bouncy(function (req, bounce) {
var host = req.headers.host
, app = App.spawn()
;
if(apps[host]) {
bounce(apps[host].ihost || currentHost(), apps[host].port);
return;
}
app.query = {host: host.replace('.deploydapp.com', '')};
app
.notify(function(json) {
apps[host] = json;
if(json.ihost && json.port) {
bounce(app.ihost, json.port);
} else if(json._id) {
// app exists, but not on any internal hosts
// set the internal host to the machine that spawned it
json.ihost = currentHost();
json.port = nextPort();
console.log(['deployd', "'" + JSON.stringify(json) + "'"].join(' '));
// then boot
var d = exec(['deployd', "'" + JSON.stringify(json) + "'"].join(' '));
d.stdout
.on('data', function(data) {
console.log(data.toString());
if(data.toString().indexOf('listening') > -1) {
bounce(json.port);
}
});
d.stderr.on('data', function(data) {
console.log(data.toString());
});
app.set(json).save();
} else {
var res = bounce.respond();
res.writeHead(500, {
'Content-Length': ERROR.length,
'Content-Type': 'text/html'
});
res.end(ERROR);
}
})
.fetch()
;
}).listen(80);
function nextPort() {
process.last = (3001 || process.last);
return process.last++;
}
function currentHost() {
return 'localhost';
}

View File

@@ -1,49 +0,0 @@
var app = require('../../app')
, App = require('./app')
;
if(process.argv.length < 3) {
require('./balancer');
}
app.post('/app', function(req, res) {
var session = req.session
, me = session && session.user && session.user.email
;
App
.spawn()
.for(req)
.set({name: req.param('name'), creator: me})
.notify(res)
.save()
;
});
app.get('/app/:id', function(req, res) {
App
.spawn()
.for(req)
.set({_id: req.param('id')})
.notify(res)
.fetch()
;
});
app.del('/app/:id', function(req, res) {
App
.spawn()
.for(req)
.notify(res)
.remove()
;
});
// views
app.get('/my/apps', function(req, res) {
res.render(__dirname + '/views/index.ejs');
});
app.get('/login', function(req, res) {
res.render(__dirname + '/views/login.ejs');
});

View File

@@ -1,29 +0,0 @@
<div class="authed">
<h2>My Apps</h2>
<h3>alpha!</h3>
<ul id="apps">
</ul>
<h4>Create an app...</h4>
<input type="text" id="name" />
<button id="create">create new</button>
</div>
<script>
d('/search/apps', {creator: 'skawful@gmail.com'}, function(res) {
$.each(res.results, function(i, item) {
$('#apps').append(
$('<li />').text(item.name + ' @ ' + item.host + '.deploydapp.com')
)
})
});
$('#create').click(function() {
var name = $('#name').val();
d('/app', {name: name}, function(res) {
console.log(res);
if(res._id) window.location.reload();
})
});
</script>

View File

@@ -1,26 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>index</title>
<meta name="generator" content="TextMate http://macromates.com/">
<meta name="author" content="ritchie">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
<script src="/deployd.js"></script>
<script src="/libs/jquery.cookie.js"></script>
<style>
.authed, .not-authed {display: none;}
</style>
</head>
<body>
<h1>Deployd</h1>
<%- body %>
<script>
var active = $.cookie('deployd.sid') ? '.authed' : '.not-authed';
console.log(active);
$(active).show();
</script>
</body>
</html>

View File

@@ -1,37 +0,0 @@
<div id="login-form">
Username
<input id="uid" type="text" />
Password
<input id="password" type="password" />
<button id="login">login</button>
</div>
<script>
d('/me', function(res) {
if(res && res.uid)
$('#login-form').html('Logged in as ' + res.uid);
})
$('#login').click(function() {
var info = {
email: $('#uid').val(),
password: $('#password').val()
};
d('/user/login', info, function(res) {
if(res.auth) {
window.location = '/my/apps';
} else {
alert('login failed...');
}
});
});
$('#logout').click(function() {
d('/user/logout', function() {
window.location.reload();
});
})
</script>

View File

@@ -1,32 +0,0 @@
var Settings = require('../settings/settings')
, Setting = require('../settings/settings')
, Model = require('../../model')
;
var models = module.exports.models = {};
var refresh = module.exports.refresh = function() {
Settings
.spawn()
.find({plugin: 'graph'})
.notify(function(model) {
var m = models[model.name] = Model.spawn();
m.description = model.description;
m.allowed = model.allowed;
m.collection = model.collection;
m.plugin = model.plugin;
})
.fetch()
;
}
// initial refresh
refresh();
// create or update general graph settings
Setting
.spawn()
.set({plugin: 'graph', name: 'graph'})
.save()
;

View File

@@ -2,19 +2,10 @@ var app = require('../../app')
, config = require('../../config').load()
, Settings = require('./settings')
, Setting = require('./setting')
, graph = require('../graph')
, models = require('../models')
, Model = require('../../model')
;
app.get('/settings', function(req, res) {
Settings
.spawn()
.for(req)
.notify(res)
.fetch()
;
});
app.post('/settings', function(req, res) {
Settings
.spawn()
@@ -22,7 +13,7 @@ app.post('/settings', function(req, res) {
.find({name: req.body.name, plugin: req.body.plugin})
.set(req.body)
.notify(function(json) {
if(req.body.plugin === 'graph') graph.refresh();
if(req.body.plugin === 'models') models.refresh();
else if(json.collection) Model.refreshSettings(json.collection);
res.send(json);
})

View File

@@ -3,7 +3,7 @@ var Model = require('../../model');
module.exports = Model.spawn({
collection: 'settings',
plugin: 'setting',
plugin: 'settings',
description: {
name: {type: 'string'},

View File

@@ -1,5 +1,6 @@
var Collection = require('../../collection');
module.exports = Collection.spawn({
collection: 'settings'
collection: 'settings',
plugin: 'settings'
});

View File

@@ -1,34 +0,0 @@
var Model = require('../../model');
var Group = module.exports = Model.spawn({
collection: 'groups',
plugin: 'user',
description: {
name: {type: 'string', unique: true},
creator: 'string'
},
allowed: {
read: 'root',
write: 'creator',
remove: 'creator',
create: 'root'
}
});
var defaults = ['root', 'admin', 'public'];
defaults.forEach(function(group) {
var g = {name: group, creator: 'root'};
Group
.spawn()
.unlock()
.find(g)
.set(g)
.save()
;
});

View File

@@ -1,82 +0,0 @@
var app = require('../../app')
, Group = require('./group')
, User = require('./user')
, Users = require('./users')
, ObjectID = require('mongodb').BSON
;
function user(action, params, req, res) {
User
.spawn()
.for(req)
.set(params)
.notify(res)
[action]()
;
}
app.post('/user', function(req, res) {
user('save', req.body, req, res);
});
app.post('/user/login', function(req, res) {
user('login', req.body, req, {
send: function(u) {
u.auth = req.sessionID;
res.send(req.session.user = u);
}
});
});
app.get('/user/logout', function(req, res) {
req.session.destroy(function() {
res.send({auth: null});
});
});
app.get('/me', function(req, res) {
user('fetch', {email: req.session.user && req.session.user.email}, req, res);
});
app.del('/me', function(req, res) {
var u = req.session.user;
if(u) user('remove', u, req, res);
});
app.get('/user/:id', function(req, res) {
User
.spawn()
.for(req)
.find({_id: req.param('id')})
.notify(res)
.fetch()
;
});
app.post('/user/:email/group', function(req, res) {
var changes = {}
, group = req.body && req.body.group
;
// TODO validate group
changes['groups.' + group] = 1;
User
.spawn()
.for(req)
.find({email: req.param('email')})
.set({$set: changes})
.notify(res)
.save()
;
});
app.get('/users', function(req, res) {
Users
.spawn({
query: req.params
})
.for(req)
.notify(res)
.fetch()
;
});

View File

@@ -1,59 +0,0 @@
var Model = require('../../model')
, ObjectID = require('mongodb').BSONPure.ObjectID
, _ = require('underscore')
;
module.exports = Model.spawn({
collection: 'users',
plugin: 'user',
description: {
email: {type: 'email', unique: true},
password: 'password',
name: 'string',
auth: 'string',
removed: 'boolean',
groups: 'object'
},
allowed: {
read: 'public',
write: 'creator',
remove: 'creator',
create: 'public',
special: {
groups: {read: 'public', write: 'root'},
email: {read: 'creator'}
}
},
toJSON: function() {
var j = Model.toJSON.apply(this, arguments);
// remove password before sending to clients
delete j.password;
return j;
},
set: function(changes) {
// prevent ever storing a real password
changes.password && (changes.password = this.hash(changes.password));
return Model.set.apply(this, arguments);
},
login: function() {
if(!this.get('email') || !this.get('password')) this.error('User email and password are required', 'Required Field');
return this.fetch();
},
hash: function(password) {
return password + 'hash!';
}
});

View File

@@ -1,8 +0,0 @@
var Collection = require('../../collection')
, User = require('./user')
;
module.exports = Collection.spawn({
collection: 'users',
model: User
});

View File

@@ -23,7 +23,7 @@ var app = {
var tests = {
'1. creating a user': {
route: '/user',
route: '/users',
data: user,
expect: {
_id: 'toExist',
@@ -34,7 +34,7 @@ var tests = {
},
'3. add a user to group': {
route: '/user/' + user.email + '/group',
route: '/users/' + user.email + '/group',
data: {group: 'root'},
expect: {
errors: 'toNotExist'
@@ -42,7 +42,7 @@ var tests = {
},
'4. login a user': {
route: '/user/login',
route: '/users/login',
data: user,
expect: {
_id: 'toExist',
@@ -102,7 +102,7 @@ var tests = {
},
'9. validate users': {
route: '/user',
route: '/users',
data: {asdf: 1234, uid: {foo: 'bar'}, password: 1111},
expect: {
errors: 'toExist'