diff --git a/README.md b/README.md
index 278c960..51408c7 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ It makes running a Node deployment server as painless as possible.
## How does it work?
-After starting Ishiki, an [API](#api) will be made available. With this API, you can deploy applications and manage
+After starting Ishiki, an [API](#api) will become available. With this API, you can deploy applications and manage
them. If your application requires a specific version of Node, it will be set up automatically for you. Each application
will run on its own IP:port internally, while being proxied through the domains specified on your app on whatever public
port you want your sites to run on (e.g. 80).
@@ -39,6 +39,19 @@ Usage:
ishiki
```
+### First time
+
+When starting Ishiki for the first time, a default admin user will be created for you and a random password will be generated.
+Ishiki should output something along the lines of:
+
+```
+Initial admin account created:
+> username: ishiki
+> password: 12345667890abcdef
+```
+
+Make sure you take good note of the password (you can change it later).
+
## Configuration
By default, Ishiki will run on the following settings:
@@ -59,6 +72,11 @@ By default, Ishiki will run on the following settings:
"database": "ishiki"
},
"logs-size": 100000,
+ "auth": {
+ "active": true,
+ "admin": "ishiki",
+ "token_expiry": 1800
+ },
"haibu": {
"env": "development",
"advanced-replies": true,
@@ -74,7 +92,7 @@ By default, Ishiki will run on the following settings:
}
```
-Copy `config.sample.js` to `config.js` and modify if you want your own settings.
+Copy `config.sample.js` to `config.js` and modify it if you want your own settings.
* `host` is the host Ishiki and its API will run on
* `port` is the port Ishiki and its API will run on
@@ -83,6 +101,7 @@ Copy `config.sample.js` to `config.js` and modify if you want your own settings.
* `port-range` is the range of ports the apps will listen on internally before being proxied
* `mongodb` is the configuration for the MongoDB database
* `logs-size` is the cap on the `log` MongoDB collection where all the user/app logs go
+* `auth` is for authentication. Set `active` to `false` to disable authentication, `admin` is the default admin username, `token_expiry` is the time in seconds a token can remain valid without activity (`false` for no expiry)
* `haibu` is whatever settings are available to the haibu module
__Running Ishiki over HTTPS__
@@ -105,8 +124,94 @@ the `-k` (or `--insecure`) flag to ignore the verification.
## API
-Ishiki provides its own API
+Ishiki provides its own API.
+
+With authentication turned on (default), all calls (except for `/users/login`) will need to explicitly specify a `token` in the URL, such as:
+```bash
+://:/?token=
+```
+
+The authentication token can be created with the help of [`/users/login`](#login)
+
+#### _Permissions_
+With the exception of logging in, permissions are as follow:
+* [__users__](#users): admins can perform any action for any user, non-admins can only update their own password
+* [__drones__](#drones): admins can performs any action for any user, non-admins can only perform actions relating to their own drones (where `/:userid` is present)
+* [__proxies__](#proxies): only admins may use this
+
+
+### Users
+
+#### `/users` (`GET`)
+Returns a list of all users
+
+#### Call example
+```bash
+curl -X GET ://:/users?token=
+```
+
+#### Response
+```json
+[ { "_id" : "51b12470b4a898d990000001",
+ "admin" : true,
+ "last_access" : "2013-06-08T22:47:22.828Z",
+ "password" : "$2a$10$TtuNxZzX3bHdQSURpLLv4OHZ1QjbW2Fy6yRs3Cv1p6w414OnoOnTi",
+ "token" : "d22b9961e33700436c76acfab2051ba73276b7fb5aa9e57bb1343fc9e5b1524f",
+ "username" : "ishiki"
+ } ]
+```
+
+---
+
+#### `/users` (`POST`)
+Creates a new user, if `password` is not provided, one will be generated. Set `admin` to `true` to give the new user admin rights.
+
+##### Call example
+```bash
+curl -X POST -H 'Content-Type: application/json' -d '{"username": "myuser"}' ://:/users?token=
+```
+
+##### Response
+```json
+{ "_id" : "51b390b90808e68d93000067",
+ "admin" : false,
+ "password" : "52360f1b10488ae7",
+ "username" : "myuser"
+}
+```
+
+---
+
+
+#### `/users/login` (`POST`)
+Returns an authentication token to be used for all other calls
+
+##### Call example
+```bash
+curl -X POST -H 'Content-Type: application/json' -d '{"username": "myuser", "password": "mypassword"}' ://:/users/login
+```
+
+##### Response
+```json
+{ "token" : "f2623f7d089e58069caf123bda4eba614b30b67e20f90074bf7dfd6241e2e0e1" }
+```
+
+---
+
+#### `/users/:userid` (`POST`)
+Updates a user, non-admin users can only update their own password, admins can update any details of any users with the exception of the `username`
+
+##### Call example
+```bash
+curl -X POST -H 'Content-Type: application/json' -d '{"password": "mynewpassword"}' ://:/users/myuser?token=
+```
+
+#### Response
+```json
+{ "message" : "Updated password" }
+```
+
### Drones
#### `/drones` (`GET`)
@@ -114,7 +219,7 @@ Returns a list of all drones
##### Call example
```bash
-curl -X GET :/drones
+curl -X GET ://:/drones?token=
```
##### Response
@@ -152,7 +257,7 @@ Returns all drones for a given user
##### Call example
```bash
-curl -X GET :/drones/user1
+curl -X GET ://:/drones/user1?token=
```
##### Response
@@ -165,7 +270,7 @@ Returns drone info for given user/app
##### Call example
```bash
-curl -X GET :/drones/user1/site1
+curl -X GET ://:/drones/user1/site1?token=
```
##### Response
@@ -178,7 +283,7 @@ Returns all running drones
##### Call example
```bash
-curl -X GET :/drones/running
+curl -X GET ://:/drones/running?token=
```
##### Response
@@ -191,7 +296,7 @@ Deploys an app from a tarball for given user/app, with Curl from your app's dire
##### Call example
```bash
-tar -cz . | curl -XPOST -m 360 -sSNT- :/drones/user1/site1/deploy
+tar -cz . | curl -XPOST -m 360 -sSNT- ://:/drones/user1/site1/deploy?token=
```
##### Response
@@ -208,7 +313,7 @@ Starts a previously stopped drone for given user/app
##### Call example
```bash
-curl -X POST :/drones/user1/site1/start
+curl -X POST ://:/drones/user1/site1/start?token=
```
##### Response
@@ -244,7 +349,7 @@ Stops a running drone for given user/app
##### Call example
```bash
-curl -X POST :/drones/user1/site1/stop
+curl -X POST ://:/drones/user1/site1/stop?token=
```
##### Response
@@ -257,7 +362,7 @@ Restarts a running drone for given user/app
##### Call example
```bash
-curl -X POST :/drones/user1/site1/restart
+curl -X POST ://:/drones/user1/site1/restart?token=
```
##### Response
@@ -275,7 +380,7 @@ Returns or streams the logs for a given app with optional filtering
##### Call example - basic
```bash
-curl -X GET -H 'Content-Type: application/json' -d '{"limit": 2}' :/drones/user1/site1/logs
+curl -X GET -H 'Content-Type: application/json' -d '{"limit": 2}' ://:/drones/user1/site1/logs?token=
```
##### Response (JSON)
@@ -299,7 +404,7 @@ curl -X GET -H 'Content-Type: application/json' -d '{"limit": 2}' ::/drones/user1/site1/logs
+curl -X GET -H 'Content-Type: application/json' -d '{"stream": true}' ://:/drones/user1/site1/logs?token=
```
##### Response (plain text)
@@ -309,6 +414,7 @@ curl -X GET -H 'Content-Type: application/json' -d '{"stream": true}'
### Proxy
#### `/proxies` (`GET`)
@@ -316,7 +422,7 @@ Returns a list of all proxies and associated routes
##### Call example
```bash
-`curl -X GET :/proxies`
+curl -X GET ://:/proxies?token=
```
##### Response
@@ -348,7 +454,7 @@ Returns a list of all routes for proxy on given port
##### Call example
```bash
-curl -X GET :/proxies/80
+curl -X GET ://:/proxies/80?token=
```
##### Response example
@@ -376,7 +482,7 @@ Starts a proxy on given port
##### Call example
```bash
-curl -X POST :/proxies/1234
+curl -X POST ://:/proxies/1234?token=
```
##### Response
@@ -392,7 +498,7 @@ and target `host` and `port` provided in `POST`. Routes created like this will b
##### Call example
```bash
-curl -X POST -H 'Content-Type: application/json' -d '{"port": "12500","host": "internal.ip","domain": "my.domain"}' :/proxies/80/set
+curl -X POST -H 'Content-Type: application/json' -d '{"port": "12500","host": "internal.ip","domain": "my.domain"}' ://:/proxies/80/set?token=
```
##### Response
@@ -407,7 +513,7 @@ Stops and removes proxy and associated routes on given port
##### Call example
```bash
-curl -X POST :/proxies/1234/delete_proxy
+curl -X POST ://:/proxies/1234/delete_proxy?token=
```
##### Response
@@ -432,7 +538,7 @@ In this case `POST` can have any of the following values for matching:
##### Call example
```bash
-curl -X POST -H 'Content-Type: application/json' -d '{"domain":"my.domain"}' :/proxies/1234/delete_route
+curl -X POST -H 'Content-Type: application/json' -d '{"domain":"my.domain"}' ://:/proxies/1234/delete_route?token=
```
##### Response
@@ -447,7 +553,7 @@ Returns all routes for given user for proxy on given port
##### Call example
```bash
-curl -X GET :/proxies/80/user1
+curl -X GET ://:/proxies/80/user1?token=
```
##### Response
@@ -475,7 +581,7 @@ Returns all routes for given user/app for proxy on given port
##### Call example
```bash
-curl -X GET :/proxies/80/user1/site1
+curl -X GET ://:/proxies/80/user1/site1?token=
```
##### Response
@@ -497,7 +603,7 @@ Deletes route for given user/app for proxy on given port
##### Call example
```bash
-curl -X POST :/proxies/80/user1/site1/delete
+curl -X POST ://:/proxies/80/user1/site1/delete?token=
```
##### Response
@@ -522,11 +628,12 @@ Ishiki will use one of the ports within the proxy port range defined in your con
* [union (0.3.6)](https://github.com/flatiron/union/tree/v0.3.6)
* [flatiron (0.3.3)](https://github.com/flatiron/flatiron/tree/v0.3.3)
-* [haibu (0.9.7)](https://github.com/nodejitsu/haibu)
+* [haibu (0.10.1)](https://github.com/nodejitsu/haibu/tree/v0.10.1)
* [semver (1.1.2)](https://github.com/isaacs/node-semver/tree/v1.1.2)
-* [tar (0.1.14)](https://github.com/isaacs/node-tar/tree/v0.1.14)
-* [http-proxy (0.8.7)](https://github.com/nodejitsu/node-http-proxy/tree/v0.8.7)
-* [mongodb (1.2.x)](https://github.com/mongodb/node-mongodb-native/tree/V1.2.10)
+* [tar (0.1.17)](https://github.com/isaacs/node-tar/tree/v0.1.17)
+* [http-proxy (0.10.2)](https://github.com/nodejitsu/node-http-proxy/tree/v0.10.2)
+* [mongodb (1.2.x)](https://github.com/mongodb/node-mongodb-native/tree/V1.2.14)
+* [bcrypt (0.7.x)](https://github.com/ncb000gt/node.bcrypt.js/tree/0.7.5)
## Requirements
diff --git a/config.sample.json b/config.sample.json
index 96fa9b6..1f2960a 100644
--- a/config.sample.json
+++ b/config.sample.json
@@ -18,6 +18,11 @@
"database": "ishiki"
},
"logs-size": 100000,
+ "auth": {
+ "active": true,
+ "admin": "ishiki",
+ "token_expiry": 1800
+ },
"haibu": {
"env": "development",
"advanced-replies": true,
diff --git a/index.js b/index.js
index 792aa40..c183a08 100644
--- a/index.js
+++ b/index.js
@@ -51,6 +51,11 @@ app.config.defaults({
database: 'ishiki'
},
'logs-size': 100000,
+ auth: {
+ active: true,
+ admin: 'ishiki',
+ token_expiry: 1800
+ },
haibu: {
env: 'development',
'advanced-replies': true,
@@ -117,7 +122,6 @@ drone.packagesDir = app.config.get('haibu:directories:packages');
if (app.config.get('haibu'))
haibu.config.defaults(app.config.get('haibu'));
-
//set up proxy
var http_proxy = require('./lib/proxy').Proxy,
proxy = new http_proxy(app, haibu);
@@ -131,6 +135,15 @@ proxy.autoload();
//define routes
require('./lib/ishiki')(app, haibu, path, fs, drone, proxy);
+if (app.config.get('auth:active')) {
+ //authentication
+ var auth = require('./lib/auth').Auth,
+ user_auth = new auth(app, haibu);
+
+ //check permissions on each request
+ app.http.before.push(user_auth.check.bind(user_auth));
+}
+
//start ishiki
app.start(app.config.get('port'), app.config.get('host'), function() {
console.log('Haibu Ishiki started on ' + app.config.get('host') + ':' + app.config.get('port'));
diff --git a/lib/auth.js b/lib/auth.js
new file mode 100644
index 0000000..3d646e6
--- /dev/null
+++ b/lib/auth.js
@@ -0,0 +1,363 @@
+/*
+ The MIT License
+
+ Copyright (c) 2013 Hadrien Jouet https://github.com/grownseed
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+var crypto = require('crypto'),
+ bcrypt = require('bcrypt'),
+ userModel = require('../models/user');
+
+var Auth = exports.Auth = function(app, haibu) {
+ var self = this,
+ admin_username = app.config.get('auth:admin');
+
+ this.app = app;
+ this.haibu = haibu;
+
+ //make sure default admin user is created
+ userModel.get({username: admin_username}, function(err, users) {
+ if (users.length == 0) {
+ self.addUser({username: admin_username, admin: true}, function(err, user) {
+ if (err)
+ return console.log(err);
+
+ console.log('Initial admin account created:\n> username: ' + user.username + '\n> password: ' + user.password);
+ });
+ }
+ });
+
+ //users api
+ app.router.path('/users', function() {
+ //return all users
+ this.get(function() {
+ var route = this;
+
+ userModel.get(function(err, users) {
+ if (err)
+ return haibu.sendResponse(route.res, 500, err);
+
+ haibu.sendResponse(route.res, 200, users);
+ });
+ });
+
+ //new user
+ this.post(function() {
+ var route = this,
+ err = null;
+
+ if (route.req.body) {
+ if (!route.req.body.username || (route.req.body.username && !route.req.body.username.trim()))
+ err = 'A username is required';
+ }else{
+ err = 'New user details missing';
+ }
+
+ if (err)
+ return haibu.sendResponse(route.res, 500, {message: err});
+
+ self.addUser(route.req.body, function(err, user) {
+ if (err)
+ return haibu.sendResponse(route.res, 500, err);
+
+ haibu.sendResponse(route.res, 200, user);
+ });
+ });
+
+ //login
+ this.post('/login', function() {
+ var route = this;
+
+ self.login(route.req.body, function(err, token) {
+ if (err)
+ return haibu.sendResponse(route.res, 500, err);
+
+ haibu.sendResponse(route.res, 200, {token: token});
+ });
+ });
+
+ //switch admin status
+ this.post('/:userid', function(userid) {
+ var route = this,
+ user;
+
+ function updateUser() {
+ userModel.get({username: userid}, function(err, users) {
+ if (err)
+ return haibu.sendResponse(route.res, 500, err);
+
+ if (users.length == 0)
+ return haibu.sendResponse(route.res, 404, {message: 'User does not exist'});
+
+ userModel.edit(users[0]._id.toString(), {$set: user}, function(err, result) {
+ if (err)
+ return haibu.sendResponse(route.res, 500, err);
+
+ var update_keys = Object.keys(user),
+ msg = update_keys.length > 0 ? 'Updated ' + update_keys.join(', ') : 'Nothing to update';
+
+ haibu.sendResponse(route.res, 200, {message: msg});
+ });
+ });
+ }
+
+ //only allow self to update password if not admin
+ if (!route.req.user.admin)
+ user = {password: route.req.body.password};
+ else
+ user = route.req.body;
+
+ //don't allow username change
+ delete user.username;
+
+ if (!user.password || (user.password && !user.password.trim())) {
+ delete user.password;
+
+ updateUser();
+ }else{
+ user.password = self._encryptPassword(user.password, function(err, hash) {
+ if (err)
+ return haibu.sendResponse(route.res, 500, err);
+
+ user.password = hash;
+
+ updateUser();
+ });
+ }
+ });
+ });
+};
+
+//check permissions
+Auth.prototype.check = function(req, res, next) {
+ var self = this,
+ url = req.url.split('?'),
+ route = url[0].split('/'),
+ params = (url[1] && url[1].trim() ? url[1].split('&') : []),
+ parsed_params = {};
+
+ //parse params
+ if (params.length > 0) {
+ for (var i = 0, n = params.length; i < n; i++) {
+ var param = params[i].split('=');
+
+ parsed_params[decodeURIComponent(param[0])] = decodeURIComponent(param[1]);
+ }
+ }
+
+ function errorMsg(err, status, expired) {
+ if (expired)
+ return self.haibu.sendResponse(res, 401, {message: 'Your authentication token has expired'});
+
+ self.haibu.sendResponse(res, status, err);
+ }
+
+ function respond(err, user) {
+ if (err)
+ errorMsg(err, 500);
+
+ var now = new Date(),
+ token_expiry = self.app.config.get('auth:token_expiry'),
+ expired = false;
+
+ if (user) {
+ //check for outdated token
+ if (!user.last_access || (user.last_access && token_expiry)) {
+ if (!user.last_access || (now.getTime() - user.last_access.getTime()) / 1000 > token_expiry) {
+ expired = true;
+ }else{
+ //update token expiry
+ userModel.edit(user._id.toString(), {$set: {last_access: now}}, function(){});
+
+ //assign user to request
+ req.user = user;
+ }
+ }
+
+ //if admin, no need to check further
+ if (user.admin && !expired)
+ return next();
+ }
+
+ //0: go ahead, 1: require auth, 2: require admin
+ var require_auth = 0;
+
+ if (route.length > 1) {
+ switch (route[1]) {
+ case 'drones':
+ case 'users':
+ if (user) {
+ if (!route[2] ||
+ (route[2] &&
+ (route[2] != user.username.toString() ||
+ (route[1] == 'users' && route[3] && route[3] == 'admin'))))
+ require_auth = 2;
+ }else{
+ require_auth = 1;
+ }
+ break;
+ //proxy stuff admin only
+ case 'proxies':
+ require_auth = 2;
+ break;
+ }
+
+ if (!require_auth) {
+ return next();
+ }else{
+ if (require_auth == 1)
+ errorMsg({message: 'You need to be authenticated to access this resource'}, 401, expired);
+ else
+ errorMsg({message: 'You are not authorized to access this resource'}, 403, expired);
+ }
+ }else{
+ return next();
+ }
+ }
+
+ //only check if route needs to be checked
+ if ((route.length > 1 && ['drones', 'users', 'proxies'].indexOf(route[1]) == -1) ||
+ req.url == '/users/login')
+ return next();
+
+ //look for active token
+ if (parsed_params.token) {
+ userModel.get({token: parsed_params.token}, function(err, users) {
+ if (err)
+ return respond(err);
+
+ if (users.length == 1)
+ return respond(null, users[0]);
+
+ respond();
+ });
+ }else{
+ respond();
+ }
+};
+
+//generate a random string
+Auth.prototype._randomString = function(n, callback) {
+ if (!callback) {
+ callback = n;
+ n = 8;
+ }
+
+ crypto.randomBytes(n, function(ex, buf) {
+ callback(buf.toString('hex'));
+ });
+};
+
+//encrypt password
+Auth.prototype._encryptPassword = function(password, callback) {
+ bcrypt.genSalt(10, function(err, salt) {
+ if (err)
+ return callback(err);
+
+ bcrypt.hash(password, salt, callback);
+ });
+};
+
+//log user in and generate new token
+Auth.prototype.login = function(user, callback) {
+ var self = this;
+
+ if (!user.username || !user.password)
+ return callback({message: 'Please provide a username and a password'});
+
+ //find user
+ userModel.get({username: user.username}, function(err, users) {
+ if (err)
+ return callback(err);
+
+ //generic message to avoid figuring out usernames
+ var user_err = {message: 'Username/Password could not be matched'};
+
+ if (users.length == 0)
+ return callback(user_err);
+
+ //compare passwords
+ bcrypt.compare(user.password, users[0].password, function(err, match) {
+ if (err || !match)
+ return callback(user_err);
+
+ //save and return token
+ self._randomString(32, function(token) {
+ userModel.edit(users[0]._id.toString(), {$set: {token: token, last_access: new Date()}}, function(err, result) {
+ if (err)
+ return callback(user_err);
+
+ callback(null, token);
+ });
+ });
+ });
+ });
+};
+
+//create a new user
+Auth.prototype.addUser = function(user, callback) {
+ var self = this;
+
+ if (!user.username)
+ return callback({message: 'You need to specify a username'});
+
+ //default to non-admin
+ if (!user.admin)
+ user.admin = false;
+
+ //check username doesn't already exist and add it if not
+ function createUser(user) {
+ userModel.get({username: user.username}, function(err, users) {
+ if (err)
+ return callback(err);
+
+ if (users.length > 0)
+ return callback({message: 'Username is already taken'});
+
+ self._encryptPassword(user.password, function(err, password) {
+ if (err)
+ return callback(err);
+
+ var clear_password = user.password;
+ user.password = password;
+
+ userModel.add(user, function(err, result) {
+ if (err)
+ return callback(err);
+
+ user.password = clear_password;
+
+ callback(null, user);
+ });
+ });
+ });
+ }
+
+ //generate password if not provided
+ if (!user.password) {
+ this._randomString(function(pw) {
+ user.password = pw;
+ createUser(user);
+ });
+ }else{
+ createUser(user);
+ }
+};
\ No newline at end of file
diff --git a/lib/drone.extend.js b/lib/drone.extend.js
index 9df2347..23c44bf 100644
--- a/lib/drone.extend.js
+++ b/lib/drone.extend.js
@@ -28,12 +28,7 @@ module.exports = function (userId, appId, stream, callback) {
// Handle error caused by `zlib.Gunzip` or `tar.Extract` failure
//
function onError(err) {
- err.usage = 'tar -cvz . | curl -sSNT- HOST/deploy/USER/APP';
- err.blame = {
- type: 'system',
- message: 'Unable to unpack tarball'
- };
- return callback(err);
+ return callback({message: 'Unable to unpack tarball'});
}
function onEnd() {
@@ -71,6 +66,10 @@ module.exports = function (userId, appId, stream, callback) {
if (!pkg.user || (pkg.user && pkg.user.trim() == ''))
errors.push('`user` is required');
+ //check user in package matches authenticated user
+ if (!stream.user.admin && pkg.user && pkg.user != stream.user.username)
+ errors.push('The authenticated user does not match `user`');
+
//check for start script
if (!pkg.scripts) {
errors.push('`scripts` is required');
diff --git a/models/user.js b/models/user.js
new file mode 100644
index 0000000..16ed880
--- /dev/null
+++ b/models/user.js
@@ -0,0 +1,27 @@
+/*
+ The MIT License
+
+ Copyright (c) 2013 Hadrien Jouet https://github.com/grownseed
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+ */
+
+var BaseModel = require('./_base').BaseModel;
+
+module.exports = new BaseModel({collection: 'user'});
\ No newline at end of file
diff --git a/package.json b/package.json
index f486cfe..8251e7b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "haibu-ishiki",
- "description": "Wrapper for haibu, http-proxy and carapace with automatic node versions",
- "version": "0.1.3",
+ "description": "Node deployment server - wrapper for haibu, http-proxy and carapace with permissions and automatic node versions",
+ "version": "0.2.0",
"author": {
"name": "Hadrien Jouet",
"email": "hj@grownseed.net"
@@ -25,7 +25,8 @@
"semver": "1.1.2",
"tar": "0.1.17",
"http-proxy": "0.10.2",
- "mongodb": "1.2.x"
+ "mongodb": "1.2.x",
+ "bcrypt": "0.7.x"
},
"main": "index.js",
"preferGlobal": true,