diff --git a/lib/plugins/apps/app.js b/lib/plugins/apps/app.js
new file mode 100644
index 0000000..d2997cd
--- /dev/null
+++ b/lib/plugins/apps/app.js
@@ -0,0 +1,54 @@
+var Model = require('../../model');
+
+module.exports = Model.spawn({
+
+ collection: 'apps',
+ plugin: 'apps',
+
+ 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();
+ }
+
+});
\ No newline at end of file
diff --git a/lib/plugins/apps/apps.js b/lib/plugins/apps/apps.js
new file mode 100644
index 0000000..565a9e2
--- /dev/null
+++ b/lib/plugins/apps/apps.js
@@ -0,0 +1,9 @@
+var Collection = require('../../collection')
+ , App = require('./app')
+;
+
+module.exports = Collection.spawn({
+ collection: 'apps',
+ plugin: 'apps',
+ model: App
+});
\ No newline at end of file
diff --git a/lib/plugins/apps/balancer.js b/lib/plugins/apps/balancer.js
new file mode 100644
index 0000000..d16405a
--- /dev/null
+++ b/lib/plugins/apps/balancer.js
@@ -0,0 +1,74 @@
+// 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 = '
Not Found
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';
+}
\ No newline at end of file
diff --git a/lib/plugins/apps/index.js b/lib/plugins/apps/index.js
new file mode 100644
index 0000000..03802b5
--- /dev/null
+++ b/lib/plugins/apps/index.js
@@ -0,0 +1,49 @@
+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');
+});
\ No newline at end of file
diff --git a/lib/plugins/apps/views/index.ejs b/lib/plugins/apps/views/index.ejs
new file mode 100644
index 0000000..5f28a6b
--- /dev/null
+++ b/lib/plugins/apps/views/index.ejs
@@ -0,0 +1,29 @@
+
+
My Apps
+
alpha!
+
+
Create an app...
+
+
+
+
+
+
+
diff --git a/lib/plugins/apps/views/layout.ejs b/lib/plugins/apps/views/layout.ejs
new file mode 100644
index 0000000..34a69d8
--- /dev/null
+++ b/lib/plugins/apps/views/layout.ejs
@@ -0,0 +1,26 @@
+
+
+
+
+
+ index
+
+
+
+
+
+
+
+
+ Deployd
+ <%- body %>
+
+
+
diff --git a/lib/plugins/apps/views/login.ejs b/lib/plugins/apps/views/login.ejs
new file mode 100644
index 0000000..1634309
--- /dev/null
+++ b/lib/plugins/apps/views/login.ejs
@@ -0,0 +1,37 @@
+
+ Username
+
+ Password
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/plugins/models/index.js b/lib/plugins/models/index.js
new file mode 100644
index 0000000..77d4797
--- /dev/null
+++ b/lib/plugins/models/index.js
@@ -0,0 +1,32 @@
+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: 'models'})
+ .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: 'models', name: 'models'})
+ .save()
+;
+
+
diff --git a/lib/plugins/users/group.js b/lib/plugins/users/group.js
new file mode 100644
index 0000000..24b7928
--- /dev/null
+++ b/lib/plugins/users/group.js
@@ -0,0 +1,34 @@
+var Model = require('../../model');
+
+var Group = module.exports = Model.spawn({
+
+ collection: 'groups',
+ plugin: 'users',
+
+ 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()
+ ;
+});
\ No newline at end of file
diff --git a/lib/plugins/users/groups.js b/lib/plugins/users/groups.js
new file mode 100644
index 0000000..ce61a9d
--- /dev/null
+++ b/lib/plugins/users/groups.js
@@ -0,0 +1,9 @@
+var Collection = require('../../collection')
+ , Group = require('./group')
+;
+
+module.exports = Collection.spawn({
+ collection: 'groups',
+ plugin: 'users',
+ model: Group
+});
\ No newline at end of file
diff --git a/lib/plugins/users/index.js b/lib/plugins/users/index.js
new file mode 100644
index 0000000..b2874b0
--- /dev/null
+++ b/lib/plugins/users/index.js
@@ -0,0 +1,83 @@
+var app = require('../../app')
+ , Group = require('./group')
+ , Groups = require('./groups')
+ , 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('/users', function(req, res) {
+ user('save', req.body, req, res);
+});
+
+app.post('/users/login', function(req, res) {
+ user('login', req.body, req, {
+ send: function(u) {
+ u.auth = req.sessionID;
+ res.send(req.session.user = u);
+ }
+ });
+});
+
+app.get('/users/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('/users/:id', function(req, res) {
+ User
+ .spawn()
+ .for(req)
+ .find({_id: req.param('id')})
+ .notify(res)
+ .fetch()
+ ;
+});
+
+app.post('/users/: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()
+ ;
+});
diff --git a/lib/plugins/users/user.js b/lib/plugins/users/user.js
new file mode 100644
index 0000000..2c86817
--- /dev/null
+++ b/lib/plugins/users/user.js
@@ -0,0 +1,62 @@
+var Model = require('../../model')
+ , ObjectID = require('mongodb').BSONPure.ObjectID
+ , _ = require('underscore')
+;
+
+module.exports = Model.spawn({
+
+ collection: 'users',
+ plugin: 'users',
+
+ 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!';
+ },
+
+ defineRoutes: function() {
+ // routes in ./index.js
+ }
+
+});
+
diff --git a/lib/plugins/users/users.js b/lib/plugins/users/users.js
new file mode 100644
index 0000000..dcc3bb4
--- /dev/null
+++ b/lib/plugins/users/users.js
@@ -0,0 +1,9 @@
+var Collection = require('../../collection')
+ , User = require('./user')
+;
+
+module.exports = Collection.spawn({
+ collection: 'users',
+ plugin: 'users',
+ model: User
+});
\ No newline at end of file