├── .gitignore ├── test ├── utils.js ├── signatures.js └── features.js ├── .travis.yml ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import stuff from 'pouchdb-plugin-helper/testutils'; 2 | import SeamlessAuth from '../'; 3 | 4 | stuff.waitUntilReady = () => SeamlessAuth(stuff.PouchDB); 5 | 6 | stuff.cleanup = async function () { 7 | await new stuff.PouchDB('_users').destroy(); 8 | } 9 | 10 | module.exports = stuff; 11 | -------------------------------------------------------------------------------- /test/signatures.js: -------------------------------------------------------------------------------- 1 | import {waitUntilReady, cleanup, PouchDB} from './utils'; 2 | 3 | describe('signatures', () => { 4 | before(waitUntilReady); 5 | afterEach(cleanup); 6 | 7 | it('seamless auth', () => { 8 | const promise = PouchDB.seamlessSession(() => {}); 9 | promise.then.should.be.ok; 10 | promise.catch.should.be.ok; 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | cache: 5 | directories: 6 | - node_modules 7 | 8 | node_js: 9 | - "0.10" 10 | 11 | services: 12 | - couchdb 13 | 14 | before_install: 15 | - npm i -g npm@^2.0.0 16 | 17 | before_script: 18 | - npm prune 19 | 20 | script: npm run $COMMAND 21 | 22 | env: 23 | matrix: 24 | - COMMAND='helper -- lint' 25 | - COMMAND='helper -- js-test' 26 | - COMMAND='build' 27 | 28 | #after_success: 29 | # - npm run helper -- semantic-release 30 | 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pouchdb-seamless-auth", 3 | "version": "2.0.1", 4 | "main": "index.js", 5 | "description": "Seamless switching between online (CouchDB) and offline (PouchDB) authentication.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/marten-de-vries/pouchdb-seamless-auth.git" 9 | }, 10 | "keywords": [ 11 | "pouch", 12 | "pouchdb", 13 | "couch", 14 | "couchdb", 15 | "users", 16 | "authentication", 17 | "auth", 18 | "seamless", 19 | "online", 20 | "offline" 21 | ], 22 | "license": "Apache-2.0", 23 | "author": "Marten de Vries", 24 | "dependencies": { 25 | "pouchdb-promise": "^0.0.0", 26 | "extend": "^3.0.0", 27 | "promise-nodify": "^1.0.0", 28 | "pouchdb-auth": "^3.0.1" 29 | }, 30 | "devDependencies": { 31 | "pouchdb-plugin-helper": "^3.0.0" 32 | }, 33 | "scripts": { 34 | "helper": "./node_modules/.bin/pouchdb-plugin-helper", 35 | "test": "npm run helper -- test", 36 | "build": "npm run helper -- build SeamlessAuth" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/features.js: -------------------------------------------------------------------------------- 1 | import {waitUntilReady, cleanup, PouchDB, should, BASE_URL, HTTP_AUTH} from './utils'; 2 | 3 | const url = BASE_URL + '/_users'; 4 | 5 | describe('sync seamless auth tests without remote', () => { 6 | before(waitUntilReady); 7 | afterEach(cleanup); 8 | 9 | it('test', async () => { 10 | const resp = await PouchDB.seamlessSignUp('username', 'password'); 11 | resp.ok.should.be.ok; 12 | const s = await PouchDB.seamlessSession(); 13 | s.info.authentication_db.should.equal('_users'); 14 | should.equal(s.userCtx.name, null); 15 | (await PouchDB.seamlessLogIn('username', 'password')).name.should.equal('username'); 16 | (await PouchDB.seamlessLogOut()).ok.should.be.ok; 17 | }); 18 | }); 19 | 20 | describe('sync seamless auth tests with remote', () => { 21 | let remoteDB, localDB; 22 | before(waitUntilReady); 23 | beforeEach(async () => { 24 | await PouchDB.setSeamlessAuthRemoteDB(url, {auth: HTTP_AUTH}); 25 | remoteDB = new PouchDB(url, {auth: HTTP_AUTH}); 26 | localDB = new PouchDB('_users'); 27 | }); 28 | afterEach(async () => { 29 | // local 30 | await cleanup(); 31 | // remote 32 | PouchDB.unsetSeamlessAuthRemoteDB() 33 | try { 34 | await remoteDB.remove(await remoteDB.get('org.couchdb.user:username')); 35 | } catch (err) {/* already not there apparently*/} 36 | }); 37 | 38 | it('test', async () => { 39 | const resp = await PouchDB.seamlessSignUp('username', 'password'); 40 | resp.ok.should.be.ok; 41 | 42 | // check replication from remote to local 43 | let doc; 44 | // ethernal loop when the test doesn't pass 45 | do { 46 | try { 47 | doc = await localDB.get(resp.id); 48 | } catch (err) {/* document not yet replicated */} 49 | } while (!doc); 50 | 51 | // check if online session 52 | (await PouchDB.seamlessLogIn('username', 'password')).name.should.equal('username'); 53 | const s = await PouchDB.seamlessSession(); 54 | s.info.authentication_handlers.should.contain('cookie'); 55 | 56 | // update the local document and check if replicated back 57 | doc.abc = 1; 58 | localDB.put(doc); 59 | 60 | // triggers the replication 61 | PouchDB.invalidateSeamlessAuthCache(); 62 | await PouchDB.seamlessSession(); 63 | 64 | let remoteDoc; 65 | do { 66 | remoteDoc = await remoteDB.get(resp.id); 67 | } while(!remoteDoc.abc); 68 | 69 | // test caching code 70 | await PouchDB.seamlessSession(); 71 | await PouchDB.seamlessSession(); 72 | 73 | // log out 74 | (await PouchDB.seamlessLogOut()).ok.should.be.ok; 75 | }); 76 | }); 77 | 78 | describe('async seamless auth tests', () => { 79 | before(waitUntilReady); 80 | afterEach(cleanup); 81 | 82 | it('set remote db', done => { 83 | PouchDB.setSeamlessAuthRemoteDB(url, {auth: HTTP_AUTH}, (err, resp) => { 84 | should.not.exist(resp); 85 | should.not.exist(PouchDB.unsetSeamlessAuthRemoteDB()); 86 | done(err); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pouchdb-seamless-auth 2 | ===================== 3 | 4 | [![Build Status](https://travis-ci.org/marten-de-vries/pouchdb-seamless-auth.svg?branch=master)](https://travis-ci.org/marten-de-vries/pouchdb-seamless-auth) 5 | [![Dependency Status](https://david-dm.org/marten-de-vries/pouchdb-seamless-auth.svg)](https://david-dm.org/marten-de-vries/pouchdb-seamless-auth) 6 | [![devDependency Status](https://david-dm.org/marten-de-vries/pouchdb-seamless-auth/dev-status.svg)](https://david-dm.org/marten-de-vries/pouchdb-seamless-auth#info=devDependencies) 7 | 8 | Seamless switching between online (CouchDB) and offline (PouchDB) 9 | authentication. 10 | 11 | **WARNING**: This plug-in stores password hashes in a local PouchDB. In 12 | for example internet cafes, this is not a smart thing to do. In your 13 | app, you should include a checkbox 'I trust this computer' and only use 14 | **pouchdb-seamless-auth** when it is checked. Otherwise, you can fall 15 | back to **pouchdb-auth**. This functionality might be implemented as 16 | part of the plug-in in the future. 17 | 18 | API 19 | --- 20 | 21 | NodeJS package name: [pouchdb-seamless-auth](https://www.npmjs.org/package/pouchdb-seamless-auth) 22 | Browser object name: ``window.SeamlessAuth`` 23 | 24 | This plug-in provides a convenience layer on top of the PouchDB Auth 25 | plug-in. By default, it users a local database named ``_users`` as 26 | backend for its log in, log out and get session actions. But, when you 27 | set a remote database, that local database is synced with the given 28 | database. In other words, it allows you to let your user log in one 29 | time using the remote database, and from that moment on you can also the 30 | session functions while offline! Very handy when using a per-user 31 | database set up that PouchDB syncs. 32 | 33 | Instead of passing this plug-in to the ``PouchDB.plugin()`` function, install 34 | it like this: 35 | 36 | ```javascript 37 | //NodeJS 38 | require("pouchdb-seamless-auth")(PouchDB) 39 | 40 | //Browser 41 | SeamlessAuth(PouchDB) 42 | ``` 43 | 44 | After that is finished (a promise is returned to help determine when that is), 45 | all functions documented below are available on the ``PouchDB`` object. 46 | 47 | ### PouchDB.setSeamlessAuthRemoteDB(remoteName[, remoteOptions[, callback]]) 48 | 49 | Set a remote database to be seamlessly synced to. 50 | 51 | **Parameters**: 52 | 53 | - *string* remoteName: The url to the remote database. Passed to the 54 | ``PouchDB`` constructor as the first argument. 55 | - *object* remoteOptions: Options to pass on to the ``PouchDB`` constructor 56 | as its second argument. 57 | - *function* callback: An alternative for the returned promise. 58 | 59 | **Returns**: a promise, which resolves to nothing when the remote database is 60 | completely set up. 61 | 62 | ### PouchDB.unsetSeamlessAuthRemoteDB() 63 | 64 | A synchronous function. Undos what ``PouchDB.setSeamlessAuthRemoteDB()`` did. 65 | 66 | **Returns**: nothing. 67 | 68 | ### PouchDB.seamlessSession([opts[, callback]]) 69 | 70 | See **pouchdb-auth**'s ``db.session()``. 71 | 72 | ### PouchDB.seamlessLogIn(username, password, [opts[, callback]]) 73 | 74 | See **pouchdb-auth**'s ``db.logIn()``. 75 | 76 | ### PouchDB.seamlessLogOut([opts[, callback]]) 77 | 78 | See **pouchdb-auth**'s ``db.logOut()``. 79 | 80 | ### PouchDB.seamlessSignUp(username, password, [opts[, callback]]) 81 | 82 | See **pouchdb-auth**'s ``db.signUp()``. 83 | 84 | ### PouchDB.invalidateSeamlessAuthCache() 85 | 86 | Used to invalidate the cache manually. 87 | 88 | This is a synchronous function. Because an application might call 89 | ``PouchDB.seamlessSession()`` a lot of times, that method is cached. For most 90 | of the time, you don't have to worry about that, because log in, log out and 91 | sign up all invalidate that cache, making it pretty much unnoticable. There is 92 | one known exception: when changing the user document in ``_users`` manually. 93 | Call this to invalidate the cache when you do that. 94 | 95 | **Returns**: nothing. 96 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014-2015, Marten de Vries 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | var Auth = require('pouchdb-auth'); 20 | var extend = require('extend'); 21 | var Promise = require('pouchdb-promise'); 22 | var nodify = require('promise-nodify'); 23 | 24 | var PouchDB, local, remote; 25 | var cacheInvalidated; 26 | var cache; 27 | 28 | module.exports = function (thePouchDB) { 29 | PouchDB = thePouchDB; 30 | local = new PouchDB('_users'); 31 | 32 | return Auth.useAsAuthenticationDB.call(local) 33 | .then(invalidateCache) 34 | .then(function () { 35 | extend(PouchDB, api); 36 | }); 37 | }; 38 | 39 | function invalidateCache(passthrough) { 40 | cacheInvalidated = true; 41 | 42 | return passthrough; 43 | } 44 | 45 | var api = {}; 46 | api.setSeamlessAuthLocalDB = function (localDB, callback) { 47 | local = localDB; 48 | 49 | var promise = Auth.useAsAuthenticationDB.call(local) 50 | .then(invalidateCache); 51 | 52 | nodify(promise, callback); 53 | return promise; 54 | }; 55 | 56 | api.unsetSeamlessAuthLocalDB = function () { 57 | local = undefined; 58 | invalidateCache(); 59 | }; 60 | 61 | api.setSeamlessAuthRemoteDB = function (remoteName, remoteOptions, callback) { 62 | remote = new PouchDB(remoteName, remoteOptions); 63 | 64 | var promise = Auth.useAsAuthenticationDB.call(remote) 65 | .then(invalidateCache); 66 | 67 | nodify(promise, callback); 68 | return promise; 69 | }; 70 | 71 | api.unsetSeamlessAuthRemoteDB = function () { 72 | remote = undefined; 73 | invalidateCache(); 74 | }; 75 | 76 | api.invalidateSeamlessAuthCache = function () { 77 | invalidateCache(); 78 | }; 79 | 80 | api.seamlessSession = function (opts, callback) { 81 | // Getting the session is something that can happen quite often in a row. HTTP 82 | // is slow (and _session is not HTTP cached), so a manual cache is implemented 83 | // here. 84 | var args = parseArgs(opts, callback); 85 | if (cacheInvalidated) { 86 | cache = callFromAvailableSource('session', args.opts) 87 | .then(function (info) { 88 | if (info.resp.userCtx.name !== null) { 89 | return startReplication(info.resp.userCtx.name, info); 90 | } else { 91 | return info; 92 | } 93 | }) 94 | .then(returnResp); 95 | cacheInvalidated = false; 96 | } 97 | nodify(cache, args.callback); 98 | return cache; 99 | }; 100 | 101 | api.seamlessLogIn = function (username, password, opts, callback) { 102 | var args = parseArgs(opts, callback); 103 | var promise = callFromAvailableSource('logIn', username, password, args.opts) 104 | .then(startReplication.bind(null, username)) 105 | .then(invalidateCache) 106 | .then(returnResp); 107 | nodify(promise, args.callback); 108 | return promise; 109 | }; 110 | 111 | api.seamlessLogOut = function (opts, callback) { 112 | var args = parseArgs(opts, callback); 113 | var promise = callFromAvailableSource('logOut', args.opts) 114 | .then(invalidateCache) 115 | .then(returnResp); 116 | nodify(promise, args.callback); 117 | return promise; 118 | }; 119 | 120 | api.seamlessSignUp = function (username, password, opts, callback) { 121 | var args = parseArgs(opts, callback); 122 | var promise = callFromAvailableSource('signUp', username, password, args.opts) 123 | .then(startReplication.bind(null, username)) 124 | .then(invalidateCache) 125 | .then(returnResp); 126 | nodify(promise, args.callback); 127 | return promise; 128 | }; 129 | 130 | function callFromAvailableSource(name/*, arg1, ...*/) { 131 | var args = Array.prototype.slice.call(arguments, 1); 132 | return Promise.resolve() 133 | .then(function () { 134 | // promisifies the 'undefined has no attribute apply' error too when in a 135 | // then-function instead of on top. 136 | return remote[name].apply(remote, args); 137 | }) 138 | .then(function (resp) { 139 | return { 140 | type: 'remote', 141 | resp: resp 142 | }; 143 | }) 144 | .catch(function () { 145 | return local[name].apply(local, args) 146 | .then(function (resp) { 147 | return { 148 | type: 'local', 149 | resp: resp 150 | }; 151 | }); 152 | }); 153 | } 154 | 155 | function returnResp(info) { 156 | return info.resp; 157 | } 158 | 159 | function parseArgs(opts, callback) { 160 | if (typeof opts === 'function') { 161 | callback = opts; 162 | opts = {}; 163 | } 164 | return { 165 | callback: callback, 166 | opts: opts 167 | }; 168 | } 169 | 170 | function startReplication(username, info) { 171 | // can't use real replication because the changes feed of _users isn't 172 | // publicly accessable for non-admins. 173 | if (info.type === 'remote') { 174 | // can only 'replicate' when the remote db is available. 175 | var getRemote = remote.get('org.couchdb.user:' + username, {revs: true}) 176 | .catch(useEmptyDoc); 177 | var getLocal = local.get('org.couchdb.user:' + username, {revs: true}) 178 | .catch(useEmptyDoc); 179 | Promise.all([getRemote, getLocal]) 180 | .then(Function.prototype.apply.bind(function (remoteDoc, localDoc) { 181 | if (remoteDoc._rev > localDoc._rev) { 182 | local.bulkDocs([remoteDoc], {new_edits: false}); 183 | } else if (remoteDoc._rev < localDoc._rev) { 184 | remote.bulkDocs([localDoc], {new_edits: false}); 185 | } else { 186 | // both were up-to-date already. Prevent cache invalidation by 187 | // returning directly. 188 | return; 189 | } 190 | invalidateCache(); 191 | }, null)); 192 | } 193 | return info; 194 | } 195 | 196 | function useEmptyDoc() { 197 | return {_rev: '0'}; 198 | } 199 | --------------------------------------------------------------------------------