mubackend

0.1.2 • Public • Published

muBackend.js

Build Status Code Climate js-semistandard-style Dependency Status npm npm

Literate programming: This documentation is and contains the entire source code.

Under development, not done yet!

MuBackend is a noBackend backend,
primarily for single page applications.
Intended features:

  • Authentication of users through different providers
  • Synchronization of user data across different clients/devices
  • Communication sending messages between users

The design criterias are: simplicity and scaleability.
This README.md, contains the entire source code,
both for the client and the server.
This implementation prioritises simplicity
over scaleability, but all of the API/algorithms
can be implemented with web-scale performance.

API

API is under implementation

Initialisation

  • mu = new MuBackend(url)
  • mu.userId - a string that identifies the user, if currently logged in
  • mu.signIn(userId, password) - login with username/password
  • mu.signInWith(provider) - login with a given provider, providers can be: "github", "twitter", "linkedin", "google", "facebook", or "wordpress". Typically called when the user clicks on a log-in button. The user leaves the page and will be redirected home to location.href when done
  • mu.signOut()

Storage

MuBackend allows creation of sync-endpoints for PouchDB.

  • mu.createDB(dbName, public) - allocates a new online database, owned by the current user. If public is true, then it will be readable by anyone. Otherwise it will only be readable by the current user.
  • mu.newPouchDB(userId, dbName, PouchDB) - returns a new PouchDB online database connected to a named db belonging to a given user. It will be read-only, unless userID is the current user. PouchDB is the PouchDB constructor. This is often just used for replication to/from a locally cached PouchDB.

Messaging

  • mu.send(user, inbox, message) - put an object to an inbox owned by a given user
  • mu.inbox(inbox) - get a pouchdb representing an inbox

Roadmap

Changelog

  • 0.1.0 Initial working version, supports login, database creation+access, and inter-user messaging, very unpolished.

Backlog

  • 0.2
    • √common.js with db-url - no promise on create
    • √remove messaging, REST instead of socket.io, (for mobile battery performance)
    • √send/inbox api
    • √icon
    • demo site
    • automated test
    • better documentation
  • later
    • Guide to install/self-host
    • Docker
    • Announce
    • video tutorial
    • example page for experimentation
    • mu.findTagged(tag) -> list of user-ids with given tag
    • mu.tagSelf(tag, true/false) -> register/deregister current user as having a tag
    • Sample applications

Versions

Even minor versions are releases, odd minor versions are development. Semi-semver

Installation

Dev-dependency on ubuntu linux: apt-get install inotify-tools couchdb npm

Sample usage

window.mu = new window.MuBackend('https://api.solsort.com/');

common.js

Shared code between client and server

exports.dbName = function(user, id) {
  return ('mu_' + user.replace(/_/g, '-') + '_' + encodeURIComponent(id))
    .toLowerCase().replace(/[^a-z0-9_$()+-]/g, '$');
}

client.js

var PouchDB = window.PouchDB

Initialisation

window.MuBackend = function MuBackend(url) {
  var self = this;
  var loginFn;
  url = url + (url[url.length -1] === '/' ? "" : "/");
  this._url = url;
  this.userId = window.localStorage.getItem('mubackend' + url + 'userId');
  this._token = window.localStorage.getItem('mubackend' + url + '_token');
  if(!this.userId && window.location.hash.indexOf("muBackendLoginToken=") !== -1) {
    var token = window.location.hash.replace(/.*muBackendLoginToken=/, "");
    this._rpc('loginToken', token, function(result) {
      result = result || {};
      if(result.user && result.token) {
        self._signIn(result.user, result.token);
      } 
    });
  }
};

MuBackend.prototype._rpc = function(name) {
  var args = Array.prototype.slice.call(arguments, 1);
  var cb = args[args.length - 1];
  var xhr = new XMLHttpRequest();
  xhr.open("POST", this._url + "mu/" + name);
  xhr.send(JSON.stringify(args.slice(0, -1)));
  xhr.onreadystatechange = function() {
    if(xhr.readyState === XMLHttpRequest.DONE) {
      if(xhr.status === 200) {
        console.log(xhr.responseText);
        cb.apply(null, JSON.parse(xhr.responseText));
      } else {
        cb("HTTP-error: " + xhr.status);
      }
    }
  };
}
MuBackend.prototype._signIn = function(userId,_token) {
  window.localStorage.setItem('mubackend' + this._url + 'userId', (this.userId = userId) || "");
  window.localStorage.setItem('mubackend' + this._url + '_token', (this._token = _token) || "");
}
MuBackend.prototype.signIn = function(userId, password) {
  var self = this;
  this._rpc('loginPassword', userId, password, function(err) {
    if(!err) {
      self._signIn(userId, password);
    }
  });
};
MuBackend.prototype.signInWith = function(provider) {
  window.location.href = this._url + 'auth/' + provider + '?' + window.location.href;
};
MuBackend.prototype.signOut = function () {
  this._signIn(undefined, undefined, undefined);
};

Storage

MuBackend.prototype.createDB = function(dbName, isPublic)  {
  this._rpc('createDB', this.userId, dbName, !isPublic, this._token, cb || function() {});
};
MuBackend.prototype.newPouchDB = function(dbName, userId)  {
  userId = userId || this.userId;
  var url = this._url + "db/" + require('./common.js').dbName(userId, dbName);
  if(this.userId) {
        url = url.replace('//', '//' +  this.userId + ':' + this._token + '@');
  }
  return new PouchDB(url);
};

Messaging

Inbox

MuBackend.prototype.send = function(user, inbox, message, cb) {
  this._rpc('send', user, inbox, message, cb || function() {});
};
MuBackend.prototype.inbox = function(inbox) {
  this.createDB("inbox_" + inbox);
  return this.newPouchDB("inbox_" + inbox);
};

Directory

MuBackend.prototype.findTagged = function(tag) {
  console.log("TODO: findTagged");
}
MuBackend.prototype.tagSelf = function(tag, t) {
  console.log("TODO: tagSelf");
}

server.js

Load config

var configFile = process.argv[process.argv.length - 1];
if (configFile.slice(-5) !== '.json') {
  console.log('Error: backend needs .json config file as argument.');
  process.exit(-1);
}
var config = require(configFile);

Default configuration

config.port = config.port || 4078;

start express server

var app = require('express')();
app.use(require('express-session')(config.expressSession));
var server = require('http').Server(app);
server.listen(config.port);

Util

var crypto = require('crypto');
var btoa = require('btoa');
var dbName = require('./common.js').dbName;
function uniqueId () { return btoa(crypto.randomBytes(12)); }
function jsonOrNull(str) { try { return JSON.parse(str);} catch(_) { return undefined; }}

CouchDB

var request = require('request');
var couchUrl = config.couchdb.url.replace('//', '//' +
    config.couchdb.user + ':' + config.couchdb.password + '@');
function getUser (user, callback) {
  request.get(couchUrl + '_users/org.couchdb.user:' + user,
      function (err, response, body) {
        callback(err ? {error: 'request error'} : JSON.parse(body));
      });
}
function createUser (user, password, meta) { // ###
  request.put({
    url: couchUrl + '_users/org.couchdb.user:' + user,
    json: {
      name: user,
      meta: meta,
      password: password,
      plain_pw: password,
      roles: [],
      type: 'user'
    }
  }, function (err, __, body) {
  });
}
function createDatabase (user, id, isPrivate, callback) { // ###
  var name = dbName(user, id);
  request.put({
    url: couchUrl + name,
    json: {}
  }, function (err, _, body) {
    callback(err || body.error);
    if (isPrivate) {
      request.put({
        url: couchUrl + name + '/_security',
        json: {'admins': { 'names': [], 'roles': [] },
          'members': {'names': [user], 'roles': []}}
      }, function (err, _, body) {
        if (err || body.error) console.log('createDatabaseSecurityError:', name, body);
      });
    } else {
      request.put({
        url: couchUrl + name + '/_design/readonly',
        json: {
          validate_doc_update: 'function(_1, _2, user){if(user.name!=="' + 
                                   user + '")throw "Forbidden";}'
        }
      }, function (err, _, body) {
        if (err || body.error) console.log('createDatabaseDesignError:', name, body);
      });
    }
  });
}
function validateUser(user, password, callback) { // ###
  request.get(couchUrl + '_users/org.couchdb.user:' + user, function (err, _, body) {
    var body = jsonOrNull(body) || {};
    if (err || password !== body.plain_pw) { callback("Login error"); } else { callback(); }
  });
}

Login

var passport = require('passport');
var loginRequests = {};

function loginHandler (provider) {
  return function (req, res) {
    passport.authenticate(provider)(req, res, function (profile) {
      if (profile.provider === 'Wordpress') profile.id = profile._json.ID;
      var user = encodeURIComponent(profile.provider + '_' + profile.id);
      if (!profile.id) {
        return res.redirect(app);
      }
      getUser(user, function (o) {
        var pw;
        if (!o.error) {
          pw = o.plain_pw;
        } else {
          pw = uniqueId();
          profile._json.loginProvider = provider;
          createUser(user, pw, profile._json);
        }

        var token = uniqueId();
        var app = req.session.app;
        loginRequests[token] = {user: user, token: pw, time: Date.now()};
        if (app.indexOf('#') === -1) {
          app += '#';
        }
        res.redirect(app + 'muBackendLoginToken=' + token);
      });
    });
  };
}

function login (access, refresh, profile, done) {
  return done(profile);
}
function addStrategy (name, Strategy, opt) {
  passport.use(new Strategy(config[name], login));
  var callbackName = 'auth/' + name + '/callback'
    config[name].callbackURL = config[name].callbackURL || config.url + callbackName;
  app.get('/auth/' + name,
      function (req, res) {
        req.session.app = req.url.replace(/^[^?]*./, '');
        return passport.authenticate(name, opt)(req, res);
      });
  app.get('/' + callbackName, loginHandler(name));
}

addStrategy('github', require('passport-github'));
addStrategy('twitter', require('passport-twitter'));
addStrategy('linkedin', require('passport-linkedin'));
addStrategy('google', require('passport-google-oauth').OAuth2Strategy, {scope: 'profile'});
addStrategy('facebook', require('passport-facebook'));
addStrategy('wordpress', require('passport-wordpress').Strategy, {scope: 'auth'});

HTTP-api

function handleHttp(name, f) { // ###
  app.all('/mu/' + name, function(req, res) {
    console.log('/moo', name);
    req.pipe(require('concat-stream')(function(body) {
      f.apply(null, (jsonOrNull(body) || []).concat([function(){
        res.end(JSON.stringify(Array.prototype.slice.call(arguments, 0)));
      }]));
    }));
  });
};
handleHttp('loginPassword', validateUser); // ###
handleHttp('loginToken', function (token, f) { // ###
  f(loginRequests[token]);
  delete loginRequests[token];
});
handleHttp('createDB', function (user, db, isPrivate, password, f) { // ###
  validateUser(user, password, function(err) {
    if(err) { f(err); } else { createDatabase(user, db, isPrivate, f); }
  });
});
handleHttp('send', function(user, inbox, msg, f) {  // ###
  request.put({ url: couchUrl + dbName(user, "inbox_" + inbox) + "/" + Date.now(), json: msg}, 
      function(err, _, body) {
        f(err, body);
      });
});

CORS

app.get('/cors/', function (req, res) {
  request.get(req.url.replace(/^[^?]*./, ''), function (_, __, body) {
    res.header('Content-Type', 'text/plain');
    res.end(body);
  });
});

Hosting of static resources

app.use('/mu/', require('express').static('./'));

create users from configfile

(function() {
  for(var user in config.createUsers) { createUser(user, config.createUsers[user]); }
})();

Readme

Keywords

none

Package Sidebar

Install

npm i mubackend

Weekly Downloads

0

Version

0.1.2

License

MIT

Last publish

Collaborators

  • rasmuserik