๐Ÿถ Account JavaScript API backed by PouchDB
JavaScript

README.md

hoodie-account-server-api

Account JavaScript API backed by PouchDB

Build Status Coverage Status Dependency Status devDependency Status

@hoodie/account-server-api is a JavaScript API to manage user accounts and authentication backed by PouchDB. Features include account profiles and tokens.

Example

var AccountApi = require('@hoodie/account-server-api')
var PouchDB = require('pouchdb')

var api = new AccountApi({
  PouchDB: PouchDB,
  usersDb: 'my-users-db',
  secret: 'secret123'
})

API

@hoodie/account-server-api is a subset of hoodie-account-client/admin. If you see any inconsistencies, please create an issue

Constructor

new AccountApi(options)
Argument Type Description Required
options.PouchDB Object PouchDB constructor Yes
options.secret String Server secret, like CouchDBโ€™s couch_httpd_auth.secret Yes
options.usersDb String Defaults to \_users No

Returns an api instance.

Examples

var PouchDB = require('pouchdb')
var api = new AccountApi({
  PouchDB: PouchDB,
  secret: 'secret123',
  usersDb: 'my-users-db'
})

api.sessions.add()

Admins can create a session for any user.

admin.sessions.add(options)
Argument Type Description Required
options.account.username String Token gets invalidated after first usage Yes (unless options.account.token set)
options.account.token String - Yes (unless options.account.username set)
options.account.password String Only applicable if options.account.username is set. If only username is passed then itโ€™s assumed that an admin wants to create a session without any validation of user credentials. No

Resolves with sessionProperties

{
  id: 'session123',
  // account is always included
  account: {
    id: 'account456',
    username: 'pat@example.com'
  }
}

Rejects with:

UnauthenticatedError Session is invalid
UnconfirmedError Account has not been confirmed yet
NotFoundError Account could not be found
Error A custom error set on the account object, e.g. the account could be blocked due to missing payments
ConnectionError Could not connect to server

Examples

// create session if patโ€™s password is "secret"
admin.sessions.add({
  account: {
    username: 'pat',
    password: 'secret'
  }
}).then(function (sessionProperties) {
  var sessionId = sessionProperties.id
  var username = sessionProperties.account.username
}).catch(function (error) {
  console.error(error)
})
// create session for pat
admin.sessions.add({
  account: {
    username: 'pat'
  }
}).then(function (sessionProperties) {
  var sessionId = sessionProperties.id
  var username = sessionProperties.account.username
}).catch(function (error) {
  console.error(error)
})
// create session using a one-time auth token
admin.sessions.add({
  account: {
    token: 'secrettoken123'
  }
}).then(function (sessionProperties) {
  var sessionId = sessionProperties.id
  var username = sessionProperties.account.username
}).catch(function (error) {
  console.error(error)
})

api.sessions.find()

admin.sessions.find(sessionId)
Argument Type Description Required
sessionId String - Yes

Resolves with sessionProperties

{
  id: 'session123',
  // account is always included
  account: {
    id: 'account456',
    username: 'pat@example.com'
    // admin accounts have no profile
  }
}

Rejects with:

UnauthenticatedError Session is invalid
NotFoundError Session could not be found
ConnectionError Could not connect to server

Example

admin.sessions.find('abc4567').then(function (sessionProperties) {
  console.log('Session is valid.')
}).catch(function (error) {
  if (error.name === 'NotFoundError') {
    console.log('Session is invalid')
    return
  }

  console.error(error)
})

api.sessions.findAll()


๐Ÿ• TO BE DONE: #27


admin.sessions.findAll(options)
Argument Type Description Required
options.include String If set to "account.profile", the profile: {...} property will be added to the response. No
options.sort String or String[] string of comma-separated list of attributes to sort by, or array of strings, see JSON API: Sorting No
options.fields Object Map of fields to include in response by type, see JSON API: Sparse Fieldset No
options.page.offset Number see JSON API: Pagination No
options.page.limit Number see JSON API: Pagination No

Resolves with Array of sessionProperties

[{
  id: 'session123',
  account: {
    id: 'account456',
    username: 'pat@example.com'
  }
}, {
  id: 'session456',
  account: {
    id: 'account789',
    username: 'sam@example.com'
  }
}]

Rejects with:

UnauthenticatedError Session is invalid
ConnectionError Could not connect to server

Example

admin.sessions.findAll()
  .then(function (sessions) {})
  .catch(function (error) {
    console.error(error)
  })

api.sessions.remove()

admin.sessions.remove(sessionId)
Argument Type Description Required
sessionId String - Yes

Resolves with sessionProperties

{
  id: 'session123',
  account: {
    id: 'account456',
    username: 'pat@example.com'
  }
}

Rejects with:

UnauthenticatedError Session is invalid
NotFoundError Session could not be found
ConnectionError Could not connect to server

Example

admin.sessions.remove('abc4567')
  .then(function (sessionProperties) {})
  .catch(function (error) {
    console.error(error)
  })

NOTE: #27 Deleting a Session does not really have an effect today, as no session state is kept, and sessions are hash based


api.sessions.removeAll()


๐Ÿ• TO BE DONE: #27


admin.sessions.removeAll(options)
Argument Type Description Required
options.sort String or String[] string of comma-separated list of attributes to sort by, or array of strings, see JSON API: Sorting No
options.fields Object Map of fields to include in response by type, see JSON API: Sparse Fieldset No
options.page.offset Number see JSON API: Pagination No
options.page.limit Number see JSON API: Pagination No

Resolves with Array of sessionProperties

[{
  id: 'session123',
  account: {
    id: 'account456',
    username: 'pat@example.com'
  }
}, {
  id: 'session456',
  account: {
    id: 'account789',
    username: 'sam@example.com'
  }
}]

Rejects with:

UnauthenticatedError Session is invalid
ConnectionError Could not connect to server

Example

admin.sessions.removeAll()
  .then(function (sessions) {})
  .catch(function (error) {
    if (error.name === 'NotFoundError') {
      console.log('Session is invalid')
      return
    }

    console.error(error)
  })

api.accounts.add()

admin.accounts.add(object)
Argument Type Required
accountProperties.username String Yes
accountProperties.password String Yes

Resolves with accountProperties:

{
  "id": "account123",
  "username": "pat",
  "createdAt": "2016-01-01T00:00:00.000Z",
  "updatedAt": "2016-01-01T00:00:00.000Z",
  "profile": {
    "fullname": "Dr. Pat Hook"
  }
}

Rejects with:

UnauthenticatedError Session is invalid
InvalidError Username must be set
ConflictError Username <username> already exists
ConnectionError Could not connect to server

Example

admin.accounts.add({
  username: 'pat',
  password: 'secret',
  profile: {
    fullname: 'Dr Pat Hook'
  }
})
  .then(function (accountProperties) {})
  .catch(function (error) {
    console.error(error)
  })

api.accounts.find()

An account can be looked up by account.id, username or token.

  • If a username property is present, it will be looked up by username
  • If an id property is present, it will be looked up by accountId
  • If an token property is present, it will be looked up by token
admin.accounts.find(idOrObject, options)
Argument Type Description Required
idOrObject String account ID. Same as {id: accountId} No
idOrObject.id String account ID. Same as passing accountId as string No
idOrObject.username String Lookup account by username No
idOrObject.token String Lookup account by one-time token No
options.include String If set to "profile", the profile: {...} property will be added to the response No

Resolves with accountProperties:

{
  "id": "account123",
  "username": "pat",
  "createdAt": "2016-01-01T00:00:00.000Z",
  "updatedAt": "2016-01-01T00:00:00.000Z",
  // if options.include === 'profile'
  "profile": {
    "fullname": "Dr. Pat Hook"
  }
}

Rejects with:

UnauthenticatedError Session is invalid
NotFoundError Account not found
ConnectionError Could not connect to server

Example

admin.accounts.find({ username: 'pat' })
  .then(function (accountProperties) {})
  .catch(function (error) {
    console.error(error)
  })

api.accounts.findAll()

admin.accounts.findAll(options)
Argument Type Description Required
options.include String If set to "profile", the profile: {...} property will be added to the response. No
options.sort String or String[] string of comma-separated list of attributes to sort by, or array of strings, see JSON API: Sorting No
options.fields Object Map of fields to include in response by type, see JSON API: Sparse Fieldset No
options.page.offset Number see JSON API: Pagination No
options.page.limit Number see JSON API: Pagination No

Resolves with Array of accountProperties

[{
  "id": "account123",
  "username": "pat",
  "createdAt": "2016-01-01T00:00:00.000Z",
  "updatedAt": "2016-01-01T00:00:00.000Z",
  // if options.include === 'profile'
  "profile": {
    "fullname": "Dr. Pat Hook"
  }
}, {
  "id": "account456",
  "username": "sam",
  "createdAt": "2016-01-01T00:00:00.000Z",
  "updatedAt": "2016-01-01T00:00:00.000Z",
  // if options.include === 'profile'
  "profile": {
    "fullname": "Lady Samident"
  }
}]

Rejects with:

UnauthenticatedError Session is invalid
ConnectionError Could not connect to server

Example

admin.accounts.findAll()
  .then(function (accounts) {})
  .catch(function (error) {
    console.error(error)
  })

api.accounts.update()

An account can be looked up by account.id, username or token.

  • If a username property is present, it will be looked up by username
  • If an id property is present, it will be looked up by accountId
  • If an token property is present, it will be looked up by token
admin.accounts.update(idOrObject, changedProperties, options)
// or
admin.accounts.update(accountProperties, options)
Argument Type Description Required
idOrObject String account ID. Same as {id: accountId} No
idOrObject.id String account ID. Same as passing accountId as string No
idOrObject.username String Lookup account by username No
idOrObject.token String Lookup account by one-time token. Token gets invalidated after first usage No
changedProperties Object Object of properties & values that changed. Other properties remain unchanged. Yes
accountProperties Object Must have an id or a username property. The userโ€™s account will be updated with the passed properties. Existing properties not passed remain unchanged. Yes
options.include String If set to "profile", the profile: {...} property will be added to the response. Defaults to "profile" if accountProperties.profile or changedProperties.profile is set. No

Resolves with accountProperties:

{
  "id": "account123",
  "username": "pat",
  "createdAt": "2016-01-01T00:00:00.000Z",
  "updatedAt": "2016-01-01T00:00:00.000Z",
  // if options.include === 'profile'
  "profile": {
    "fullname": "Dr. Pat Hook"
  }
}

Rejects with:

UnauthenticatedError Session is invalid
NotFoundError Account not found
ConnectionError Could not connect to server

Examples

admin.accounts.update({ username: 'pat' }, { foo: 'bar' })
  .then(function (accountProperties) {})
  .catch(function (error) {
    console.error(error)
  })
// same as
admin.accounts.update({ username: 'pat', foo: 'bar' })
  .then(function (accountProperties) {})
  .catch(function (error) {
    console.error(error)
  })

api.accounts.updateAll()


๐Ÿ• TO BE DONE: create issue and link it here


api.accounts.remove()

An account can be looked up by account.id, username or token.

  • If a username property is present, it will be looked up by username
  • If an id property is present, it will be looked up by accountId
  • If an token property is present, it will be looked up by token
admin.accounts.remove(idOrObject, changedProperties, options)
// or
admin.accounts.remove(accountProperties, options)
Argument Type Description Required
idOrObject String account ID. Same as {id: accountId} No
idOrObject.id String account ID. Same as passing accountId as string No
idOrObject.username String Lookup account by username No
idOrObject.token String Lookup account by one-time token No
changedProperties Object Object of properties & values that changed. Other properties remain unchanged. Yes
accountProperties Object Must have an id or a username property. The userโ€™s account will be updated with the passed properties. Existing properties not passed remain unchanged. Note that accountProperties.token is not allowed, as itโ€™s not a valid account property, but an option to look up an account. An account can have multiple tokens at once. Yes
options.include String If set to "profile", the profile: {...} property will be added to the response. Defaults to "profile" if accountProperties.profile or changedProperties.profile is set. No

Resolves with accountProperties:

{
  "id": "account123",
  "username": "pat",
  "createdAt": "2016-01-01T00:00:00.000Z",
  "updatedAt": "2016-02-01T00:00:00.000Z",
  "deletedAt": "2016-03-01T00:00:00.000Z",
  // if options.include === 'profile'
  "profile": {
    "fullname": "Dr. Pat Hook"
  }
}

Rejects with:

UnauthenticatedError Session is invalid
NotFoundError Account not found
ConnectionError Could not connect to server

Examples

admin.accounts.remove({ username: 'pat' }, { reason: 'foo bar' })
  .then(function (accountProperties) {})
  .catch(function (error) {
    console.error(error)
  })
// same as
admin.accounts.remove({ username: 'pat', reason: 'foo bar' })
  .then(function (accountProperties) {})
  .catch(function (error) {
    console.error(error)
  })

api.accounts.removeAll()


๐Ÿ• TO BE DONE: create issue and link it here


api.requests.add()


๐Ÿ• TO BE DONE: create issue and link it here


admin.requests.add({
  type: 'passwordreset',
  email: 'pat@example.com'
})

Resolves with

{
  id: 'request123',
  type: 'passwordreset',
  email: 'pat@example.com'
}

api.requests.find()


๐Ÿ• TO BE DONE: create issue and link it here


admin.requests.find('token123')
admin.requests.find({id: 'token123'})

api.requests.findAll()


๐Ÿ• TO BE DONE: create issue and link it here


admin.requests.findAll()

api.requests.remove()


๐Ÿ• TO BE DONE: create issue and link it here


admin.requests.remove('token123')
admin.requests.find({id: 'token123'})

api.requests.removeAll()


๐Ÿ• TO BE DONE: create issue and link it here


api.account()

The admin.account method returns a scoped API for one account, see below

var account = admin.account(idOrObject)

Examples

admin.account('account123')
admin.account({id: 'account123'})
admin.account({username: 'pat@example.com'})
admin.account({token: 'token456'})

api.account().profile.find()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).profile.find()

resolves with profileProperties

{
  "id": "account123-profile",
  "fullname": "Dr Pat Hook",
  "address": {
    "city": "Berlin",
    "street": "AdalberststraรŸe 4a"
  }
}

api.account().profile.update()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).profile.update(changedProperties)

resolves with profileProperties

{
  "id": "account123-profile",
  "fullname": "Dr Pat Hook",
  "address": {
    "city": "Berlin",
    "street": "AdalberststraรŸe 4a"
  }
}

api.account().tokens.add()

admin.account('account123').tokens.add(properties)
Argument Type Description Required
properties.type String Every token needs a type, for example "passwordreset" Yes
properties.timeout Number Time from now until expiration of token in seconds. Defaults to 7200 (2 hours) No

resolves with tokenProperties

{
  "id": "token123",
  "type": "passwordreset",
  "accountId": "account123",
  "contact": "pat@example.com",
  "createdAt": "2016-01-01T00:00:00.000Z"
}

Rejects with:

NotFoundError Account not found
ConnectionError Could not connect to server

Example

admin.account({username: 'pat@example.com'}).account.tokens.add({
  type: 'passwordreset',
  email: 'pat@example.com'
})

api.account().tokens.find()

admin.account(idOrObject).tokens.find(id)
Argument Type Description Required
id String token id Yes

resolves with tokenProperties

{
  "id": "token123",
  "type": "passwordreset",
  "accountId": "account123",
  "contact": "pat@example.com",
  "createdAt": "2016-01-01T00:00:00.000Z"
}

Rejects with:

NotFoundError Account not found
ConnectionError Could not connect to server

Example

admin.account({username: 'pat'}).tokens.find('token123')

api.account().tokens.findAll()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).tokens.findAll(options)

resolves with array of tokenProperties

[{
  "id": "token123",
  "type": "passwordreset",
  "accountId": "account123",
  "contact": "pat@example.com",
  "createdAt": "2016-01-01T00:00:00.000Z"
}, {
  "id": "token456",
  "type": "session",
  "accountId": "account123",
  "createdAt": "2016-01-02T00:00:00.000Z"
}]

Example

admin.account({username: 'pat'}).tokens.findAll()
  .then(function (tokens) {})
  .catch(function (error) {
    console.error(error)
  })

api.account().tokens.remove()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).tokens.remove(idOrObject)

resolves with tokenProperties

{
  "id": "token123",
  "type": "passwordreset",
  "accountId": "account123",
  "contact": "pat@example.com",
  "createdAt": "2016-01-01T00:00:00.000Z"
}

Example

admin.account({username: 'pat'}).tokens.removes('token123')

api.account().roles.add()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).roles.add(name)

resolves with roleName

"mycustomrole"

Example

admin.account({username: 'pat'}).roles.add('mycustomrole')

api.account().roles.findAll()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).roles.add(name)

resolves with array of roleNames

["mycustomrole", "myothercustomrole"]

Example

admin.account({username: 'pat'}).roles.findAll()
  .then(function (roles) {})
  .catch(function (error) {
    console.error(error)
  })

api.account().roles.remove()


๐Ÿ• TO BE DONE: create issue and link it here


admin.account(idOrObject).roles.remove(name)

resolves with roleName

"mycustomrole"

Example

admin.account({username: 'pat'}).roles.remove('mycustomrole')

Events


๐Ÿ• TO BE DONE: #35


Events emitted on

  • admin.sessions
  • admin.accounts
  • admin.requests
change triggered for any add, update and remove event
add
update
remove
admin.sessions.on('change', function (eventName, session) {})
admin.accounts.on('update', function (account) {})
admin.requests.on('remove', handler)

Contributing

Have a look at the Hoodie project's contribution guidelines. If you want to hang out you can join our Hoodie Community Chat.

Testing

Local setup

git clone https://github.com/hoodiehq/hoodie-account-server-api.git
cd hoodie-account-server-api
npm install

Run all tests and code style checks

npm test

If you want to run a single test you can do it with

./node_modules/.bin/tap test/unit/sessions/remove-test.js

License

Apache 2.0