Account JavaScript API backed by PouchDB
@hoodie/account-server-api
is a JavaScript API to manage user accounts and
authentication backed by PouchDB. Features include account profiles and tokens.
var AccountApi = require('@hoodie/account-server-api')
var PouchDB = require('pouchdb')
var api = new AccountApi({
PouchDB: PouchDB,
usersDb: 'my-users-db',
secret: 'secret123'
})
@hoodie/account-server-api
is a subset of hoodie-account-client/admin.
If you see any inconsistencies, please create an issue
- Constructor
- api.sessions.add()
- api.sessions.find()
- api.sessions.findAll()
- api.sessions.remove()
- api.sessions.removeAll()
- api.accounts.add()
- api.accounts.find()
- api.accounts.findAll()
- api.accounts.update()
- api.accounts.updateAll()
- api.accounts.remove()
- api.accounts.removeAll()
- api.requests.add()
- api.requests.find()
- api.requests.findAll()
- api.requests.remove()
- api.requests.removeAll()
- api.account()
- api.account().profile.find()
- api.account().profile.update()
- api.account().tokens.add()
- api.account().tokens.find()
- api.account().tokens.findAll()
- api.account().tokens.remove()
- api.account().roles.add()
- api.account().roles.findAll()
- api.account().roles.remove()
- Events
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'
})
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 |
options.timeout |
Number | Time from now until expiration of session in seconds. Defaults to no timeout. | No |
Resolves with sessionProperties
{
id: 'session123',
// account is always included
account: {
id: 'account456',
username: '[email protected]'
}
}
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)
})
admin.sessions.find(sessionId)
Argument | Type | Description | Required |
---|---|---|---|
sessionId |
String | - | Yes |
Resolves with sessionProperties
{
id: 'session123',
// account is always included
account: {
id: 'account456',
username: '[email protected]'
// 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)
})
🐕 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: '[email protected]'
}
}, {
id: 'session456',
account: {
id: 'account789',
username: '[email protected]'
}
}]
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)
})
admin.sessions.remove(sessionId)
Argument | Type | Description | Required |
---|---|---|---|
sessionId |
String | - | Yes |
Resolves with sessionProperties
{
id: 'session123',
account: {
id: 'account456',
username: '[email protected]'
}
}
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
🐕 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: '[email protected]'
}
}, {
id: 'session456',
account: {
id: 'account789',
username: '[email protected]'
}
}]
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)
})
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)
})
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)
})
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)
})
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)
})
🐕 TO BE DONE: create issue and link it here
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)
})
🐕 TO BE DONE: create issue and link it here
🐕 TO BE DONE: create issue and link it here
admin.requests.add({
type: 'passwordreset',
email: '[email protected]'
})
Resolves with
{
id: 'request123',
type: 'passwordreset',
email: '[email protected]'
}
🐕 TO BE DONE: create issue and link it here
admin.requests.find('token123')
admin.requests.find({id: 'token123'})
🐕 TO BE DONE: create issue and link it here
admin.requests.findAll()
🐕 TO BE DONE: create issue and link it here
admin.requests.remove('token123')
admin.requests.find({id: 'token123'})
🐕 TO BE DONE: create issue and link it here
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: '[email protected]'})
admin.account({token: 'token456'})
🐕 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"
}
}
🐕 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"
}
}
admin.account('account123').tokens.add(properties)
Argument | Type | Description | Required |
---|---|---|---|
properties.type
|
String | Every token needs a type, for example "passwordreset" |
Yes |
properties.id
|
String | Optional token id. If none is passed, a UUID will be generated | No |
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": "[email protected]",
"createdAt": "2016-01-01T00:00:00.000Z"
}
Rejects with:
NotFoundError |
Account not found |
---|---|
ConnectionError |
Could not connect to server |
Example
admin.account({username: '[email protected]'}).account.tokens.add({
type: 'passwordreset',
email: '[email protected]'
})
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": "[email protected]",
"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')
🐕 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": "[email protected]",
"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)
})
🐕 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": "[email protected]",
"createdAt": "2016-01-01T00:00:00.000Z"
}
Example
admin.account({username: 'pat'}).tokens.removes('token123')
🐕 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')
🐕 TO BE DONE: create issue and link it here
admin.account(idOrObject).roles.add(name)
resolves with array of roleName
s
["mycustomrole", "myothercustomrole"]
Example
admin.account({username: 'pat'}).roles.findAll()
.then(function (roles) {})
.catch(function (error) {
console.error(error)
})
🐕 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')
🐕 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)
Have a look at the Hoodie project's contribution guidelines. If you want to hang out you can join our Hoodie Community Chat.
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