Skip to content

Commit

Permalink
feat: initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
gr2m committed Oct 15, 2016
1 parent 2b05a27 commit 3798614
Show file tree
Hide file tree
Showing 21 changed files with 530 additions and 0 deletions.
55 changes: 55 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
module.exports = accountApi

var EventEmitter = require('events').EventEmitter

var setup = require('./lib/setup')

var addSession = require('./lib/sessions/add')
var findSession = require('./lib/sessions/find')
var removeSession = require('./lib/sessions/remove')

var addAccount = require('./lib/accounts/add')
var findAccount = require('./lib/accounts/find')
var findAllAccounts = require('./lib/accounts/find-all')
var updateAccount = require('./lib/accounts/update')
var removeAccount = require('./lib/accounts/remove')
var accountsOn = require('./lib/accounts/on')

var startListeningToAccountChanges = require('./lib/utils/start-listening-to-account-changes')

function accountApi (options) {
var accountsEmitter = new EventEmitter()
var state = {
db: options.db,
secret: options.secret,
accountsEmitter: accountsEmitter
}

// returns promise
var setupPromise = setup(state)

accountsEmitter.once('newListener', startListeningToAccountChanges.bind(null, state))

return {
sessions: {
add: promiseThen.bind(null, setupPromise, addSession.bind(null, state)),
find: promiseThen.bind(null, setupPromise, findSession.bind(null, state)),
remove: promiseThen.bind(null, setupPromise, removeSession.bind(null, state))
},
accounts: {
add: promiseThen.bind(null, setupPromise, addAccount.bind(null, state)),
find: promiseThen.bind(null, setupPromise, findAccount.bind(null, state)),
findAll: promiseThen.bind(null, setupPromise, findAllAccounts.bind(null, state)),
remove: promiseThen.bind(null, setupPromise, removeAccount.bind(null, state)),
update: promiseThen.bind(null, setupPromise, updateAccount.bind(null, state)),
on: accountsOn.bind(null, state)
}
}
}

function promiseThen (promise, method) {
var args = Array.prototype.slice.call(arguments, 2)
return promise.then(function () {
return method.apply(null, args)
})
}
39 changes: 39 additions & 0 deletions lib/accounts/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module.exports = addAccount

var uuid = require('uuid')

var errors = require('../utils/errors')
var toAccount = require('../utils/doc-to-account')

function addAccount (state, properties, options) {
if (!options) {
options = {}
}
var accountKey = 'org.couchdb.user:' + properties.username
var accountId = properties.id || uuid.v4()

var doc = {
_id: accountKey,
type: 'user',
name: properties.username,
password: properties.password,
roles: [
'id:' + accountId
].concat(properties.roles || [])
}

return state.db.put(doc)

.catch(function (error) {
if (error.status === 409) {
throw errors.USERNAME_EXISTS
}
throw error
})

.then(function () {
return toAccount(doc, {
includeProfile: options.include === 'profile'
})
})
}
20 changes: 20 additions & 0 deletions lib/accounts/find-all.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = findAllAccount

var toAccount = require('../utils/doc-to-account')

function findAllAccount (state, options) {
return state.db.allDocs({
include_docs: true,
startkey: 'org.couchdb.user:',
// https://wiki.apache.org/couchdb/View_collation#String_Ranges
endkey: 'org.couchdb.user:\ufff0'
})

.then(function (response) {
return response.rows.map(function (row) {
return toAccount(row.doc, {
includeProfile: options.include === 'profile'
})
})
})
}
17 changes: 17 additions & 0 deletions lib/accounts/find.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = findAccount

var findUserDoc = require('../utils/find-user-by-username-or-id')
var toAccount = require('../utils/doc-to-account')

function findAccount (state, idOrObject, options) {
if (!options) {
options = {}
}
return findUserDoc(state.db, idOrObject)

.then(function (doc) {
return toAccount(doc, {
includeProfile: options.include === 'profile'
})
})
}
5 changes: 5 additions & 0 deletions lib/accounts/on.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = on

function on (state, eventName, handler) {
state.accountsEmitter.on(eventName, handler)
}
9 changes: 9 additions & 0 deletions lib/accounts/remove.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = removeAccount

var updateAccount = require('./update')

function removeAccount (state, idOrObject, options) {
return updateAccount(state, idOrObject, {
_deleted: true
}, options)
}
63 changes: 63 additions & 0 deletions lib/accounts/update.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module.exports = updateAccount

var _ = require('lodash')

var findUserDoc = require('../utils/find-user-by-username-or-id')
var toAccount = require('../utils/doc-to-account')

function updateAccount (state, idOrObject, change, options) {
if (!options) {
options = {}
}
return findUserDoc(state.db, idOrObject)

.then(function (doc) {
var username = change.username

if (username) {
// changing the username requires 2 operations:
// 1) create a new doc (with the new name)
// 2) delete the old doc

var oldDoc = doc

// the new doc will NOT include the username or the _rev
doc = _.merge(
_.omit(doc, ['username', '_rev']),
_.omit(change, 'username'),
{_id: 'org.couchdb.user:' + username, name: username}
)

// 1) add the new doc
return state.db.put(doc)

.then(function (response) {
doc._rev = response.rev

// delete the old doc and add the renamedTo field
var deletedDoc = _.defaultsDeep({
_deleted: true,
renamedTo: username
}, oldDoc)

// 2) delete the old doc
return state.db.put(deletedDoc)
})

.then(() => doc)
}

return state.db.put(_.merge(doc, change))

.then(function (response) {
doc._rev = response.rev
return doc
})
})

.then(function (doc) {
return toAccount(doc, {
includeProfile: options.include === 'profile'
})
})
}
19 changes: 19 additions & 0 deletions lib/db/users-design-doc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* global emit */
module.exports = {
_id: '_design/byId',
views: {
byId: {
map: function (doc) {
var isAdmin = doc.roles.indexOf('_admin') !== -1
if (isAdmin) {
return
}
for (var i = 0; i < doc.roles.length; i++) {
if (doc.roles[i].substr(0, 3) === 'id:') {
return emit(doc.roles[i].substr(3), null)
}
}
}.toString()
}
}
}
57 changes: 57 additions & 0 deletions lib/sessions/add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
module.exports = addSession

var calculateSessionId = require('couchdb-calculate-session-id')

var errors = require('../utils/errors')
var validatePassword = require('../utils/validate-password')
var toAccount = require('../utils/doc-to-account')

function addSession (state, options) {
return state.db.get('org.couchdb.user:' + options.username)

.then(function (doc) {
// no auth, skip authentication (act as admin)
if (!options.auth) {
return doc
}

return new Promise(function (resolve, reject) {
validatePassword(
options.auth.password,
doc.salt,
doc.iterations,
doc.derived_key,
function (error, isCorrectPassword) {
if (error) {
return reject(error)
}

if (!isCorrectPassword) {
return reject(errors.UNAUTHORIZED_PASSWORD)
}

resolve(doc)
}
)
})
})

.then(function (doc) {
var sessionTimeout = 1209600 // 14 days
var sessionId = calculateSessionId(
doc.name,
doc.salt,
state.secret,
Math.floor(Date.now() / 1000) + sessionTimeout
)

var session = {
id: sessionId,
account: toAccount(doc, {
includeProfile: options.include === 'account.profile'
})
}

return session
})
}
36 changes: 36 additions & 0 deletions lib/sessions/find.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module.exports = findSession

var decodeSessionId = require('../utils/couchdb-decode-session-id')
var errors = require('../utils/errors')
var isValidSessionId = require('../utils/couchdb-is-valid-session-id')
var toAccount = require('../utils/doc-to-account')

function findSession (state, id, options) {
if (!options) {
options = {}
}
var username = decodeSessionId(id).name

return state.db.get('org.couchdb.user:' + username)

.then(function (user) {
if (!isValidSessionId(state.secret, user.salt, id)) {
throw errors.MISSING_SESSION
}

return user
})

.then(function (doc) {
var account = toAccount(doc, {
includeProfile: options.include === 'account.profile'
})

var session = {
id: id,
account: account
}

return session
})
}
13 changes: 13 additions & 0 deletions lib/sessions/remove.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = removeSession

var findSession = require('./find')

function removeSession (state, id, options) {
return findSession(state, id, options)

.then(function (session) {
if (options.include) {
return session
}
})
}
13 changes: 13 additions & 0 deletions lib/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = setup

var usersDesignDoc = require('./db/users-design-doc')

function setup (state) {
return state.db.put(usersDesignDoc)

.catch(function (error) {
if (error.name !== 'conflict') {
throw error
}
})
}
12 changes: 12 additions & 0 deletions lib/utils/couchdb-decode-session-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = decodeSessionId

var base64url = require('base64url')

function decodeSessionId (id) {
var parts = base64url.decode(id).split(':')
return {
name: parts[0],
time: parseInt(parts[1], 16),
token: parts[2]
}
}
13 changes: 13 additions & 0 deletions lib/utils/couchdb-is-valid-session-id.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = isValidSessionId

var calculateSessionId = require('couchdb-calculate-session-id')
var decodeSessionId = require('./couchdb-decode-session-id')

function isValidSessionId (secret, salt, sessionId) {
var session = decodeSessionId(sessionId)
var name = session.name
var time = session.time
var sessionIdCheck = calculateSessionId(name, salt, secret, time)

return sessionIdCheck === sessionId
}
Loading

0 comments on commit 3798614

Please sign in to comment.