A user management and authentication library for Express apps.
It automatically creates and adds the following API endpoints to an Express app:
Additional features include:
New in V3.0.0: Support for Hooks
init
methodnpm install --save express-user-manager
const express = require('express');
const userManager = require('express-user-manager');
const app = express();
/**
* Setup the datastore using any of the currently supported database adapters:
* - mongoose: for MongoDB
* - sequelize: for any of the other supported database engines:
* MySQL | MariaDB | SQLite | Microsoft SQL Server | Postgres | In-memory DB
* (See the section on "Built-in data stores" for supported database engines)
*/
const dbAdapter = 'mongoose'; // OR 'sequelize'
const store = userManager.getDbAdapter(dbAdapter);
// Bind the routes under [apiMountPoint] (default: ***/api/users***):
userManager.listen(expressApp, apiMountPoint = '/api/users', customRoutes = {});
(async function() {
const server = http.createServer(app);
// Establish a connection to the data store
// Ensure the db is connected before binding the server to the port
await store.connect({
host: DB_HOST, // optional, default: 'localhost'
port: DB_PORT, // optional
user: DB_USERNAME, // optional
pass: DB_PASSWORD, // optional
engine: DB_ENGINE, // optional if the adapter is "mongoose" or if the value is "memory" and the adapter is "sequelize"; required otherwise
dbName: DB_DBNAME, // optional, default: 'users'
storagePath: DB_STORAGE_PATH, // optional, required if "engine" is set to "sqlite"
debug: DB_DEBUG, // optional, default: false
exitOnFail: EXIT_ON_DB_CONNECT_FAIL // optional, default: true
});
// Proceed with normal server initialization tasks
server.listen(PORT);
server.on('error', onError);
server.on('listening', onListening);
})();
// Optionally listen for and handle events
// (See the Emitted events section for more)
userManager.on(EVENT_NAME, function(data) {
// do something with data
});
Quick notes
expressApp
parameter has the following constraints:
const app = express()
)http.createServer(app)
apiMountPoint
parameter allows you to specify the base API route.
Every request to the API will be relative to this base route. The default is /api/users
.The customRoutes
parameter is an object that allows customization of the routes.
(See Specifying custom API endpoints for more)
If your expressApp has its own custom routing in place,
make sure to call userManager.listen(expressApp)
before setting up your app's custom 404 handler.
This is because your app's 404 handler is meant to trap requests sent to routes that have no explicit handler in your app's routing system.
Consequently, if you setup your app's custom 404 handler before calling userManager.listen()
,
requests to routes handled by the userManager
's routing system will never get to it as they will be trapped and handled by your 404 handler.
init
methodThe init
method provides a shortcut way to perform the setup and initialization steps above.
It is an async
function that runs setup and initialization tasks, connects to the database, then starts listening for requests,
all in a single step: await init(app, options);
.
It takes two parameters:
config
method)
as the second argument.express-user-manager can be configured in several ways:
config
method (See The config method)init(app, options)
methodconfig
method
config
, then configuration values are searched in environment variables.config
, then the others are searched for in environment variables.config
take precedence over environment variables.listen
or init
methods take precedence over configuration options set using config
.NODE_ENV
(string): The environment in which the app is running: development, production, staging, test, etc.API_MOUNT_POINT
(string): The route under which to listen for API requests, default is: /api/users
PORT
: The port on which the server is running (or should run, if using as a stand-alone server)DB_ENGINE
: The database engine to use. Should be one of the supported databases.
(See Built-in data stores)DB_ADAPTER
: The adapter to use. Set it to mongoose
if using MongoDB; Set it to sequelize
otherwise.DB_STORAGE_PATH
: Define this only when the DB_ENGINE is set to sqlite
.DB_HOST
: The database hostDB_USERNAME
: The database userDB_PASSWORD
: The database user's passwordDB_DBNAME
: The name of the databaseDB_PORT
: The port on which the database is runningDB_DEBUG
: Set to true
or a non-zero integer to display debug output for the database.EXIT_ON_DB_CONNECT_FAIL
: Set to true
or a non-zero integer if the app should exit if it is unable to establish a connection to the database.SESSION_SECRET
(string)AUTH_TOKEN_SECRET
(string)AUTH_TOKEN_EXPIRY
(number): Authorization token expiry (in seconds)PASSWORD_MIN_LENGTH
(number)PASSWORD_MAX_LENGTH
(number)DISALLOWED_PASSWORDS
: (array): A comma-separated list of weak/non-secure passwords that should not allowed to be used as passwordsNote: express-user-manager uses the dotenv package, so a quick and easy way to define the above variables is to create a .env file at the root of your project directory, and add them to the file and they will automatically be picked up. Sample .env file
config
methodAs stated earlier in the Configuration section,
one of the ways you can configure express-user-manager is by using the config
method.
This method provides an alternate way to pass configuration values to *express-user-manager** if you haven't done (or are unable to do) so via environment variables.
Below is an example touching on every setting:
apiMountPoint
: (string) specifies the users API routes base routepassword
: (object) for configuring minimum and maximum password length, as well as disallowed passwordsroutes
: (object) for setting up custom API endpointsdb
: (object) encapsulating database connection informationsecurity
: (object) for configuring session and authorization tokens and expiryconst express = require('express');
const userManager = require('express-user-manager');
const app = express();
const dbAdapter = 'mongoose'; // OR 'sequelize'
// Call config(options) to configure the app
userManager.config({
apiMountPoint: {string}, // The base route under which to listen for API requests
password: { // {object} for password configuration
minLength: {number}, // minimum length of user passwords, default: 6,
maxLength: {number}, // maximum length of user passwords, default: 20
disallowed: {string | array}, // comma-separated string or array of strings considered weak/non-secure passwords
},
routes: { // {object} for configuring custom routes, with members
list: {string}, // specifies the path for getting users listing
search: {string}, // specifies the path for searching users
getUser: {string}, // specifies the path for getting a user's details via their username, a /:{username} is appended to this path
signup: {string}, // specifies the user registration path
login: {string}, // specifies user authentication path,
logout: {string}, // defines the logout path
updateUser: {string}, // specifies the path for updating a user's data
deleteUser: {string} // specifies the path for deleting a user, a /:{userId} is appended to this path
},
db: { // {object} for configuring the database connection
adapter: {string}, // the adapter to use. valid values include 'mongoose', 'sequelize'
host: {mixed}, // database host
port: {number}, // database port
user: {string}, // database user
pass: {string}, // database user's password
engine: {string}, // the database engine, when the adapter is set to "sequelize". values: 'memory', 'mariadb', 'mssql', 'mysql', 'postgres', 'sqlite'
dbName: {string}, // name of the database to connect to
storagePath: {string}, // the database storage path, only valid when "engine" is "sqlite". combined with `dbName`: `${storagePath}/${dbName}.sqlite`
debug: {boolean}, // a value of true outputs database debug info
exitOnFail: {boolean}, // set to true to kill the Node process if database connection fails
},
security: { // {object} for configuring security
sessionSecret: {string}, // a key for encrypting the session
authTokenSecret: {string}, // a key for signing the authorization token
authTokenExpiry: {number}, // the expiry time of the authorization token (in seconds), example: 60 * 60 * 24
}
});
async(() => {
/**
* The dbAdapter argument is not required if it is either:
* - specified in the db section of the call to config or as
* - set using the DB_ADAPTER environment variable
*/
const store = userManager.getDbAdapter([dbAdapter]);
/**
* The connectionOptions are not required if the values:
* - are already specified in the db section of the call to config
* - are set using the DB_* environment variables
*/
await store.connect([connectionOptions]);
// If the dbAdapter and connection values are already specified
// via config or via environment variables,
// then the above two calls can be tersely combined in a single call:
// await userManager.getDbAdapter().connect();
});
// Bind request listeners
userManager.listen(expressApp);
Notes on configuration settings:
config
if they are already defined using environment variables.
The exception to this is routes
, which cannot be set via an environment variable.
However, you can set it using the third parameter to the call to listen(app, apiMountPoint, routes)
.config
,
the value set in config
will take precedence and be used instead.apiMountPoint
can be set (in increasing order of precedence):
apiMountPoint
key in the options to config
listen(app, apiMountPoint, routes)
To customize the request paths, either:
pass a routes
property with the API endpoints to config
:
userManager.config({
routes: customApiEndpoints
});
userManager.listen(expressApp, apiMountPoint);
userManager.listen
:
userManager.listen(expressApp, apiMountPoint, customApiEndpoints)
Below is the default definition of the API Endpoints, which can be modified for your custom routes:
const customApiEndpoints = {
list : '/', // Resolves to [apiMountPoint]/
search : '/search', // Resolves to [apiMountPoint]/search
getUser : '/user', // Resolves to [apiMountPoint]/user/:username
signup : '/', // Resolves to [apiMountPoint]/
login : '/login', // Resolves to [apiMountPoint]/login
logout : '/logout', // Resolves to [apiMountPoint]/logout
updateUser : '/', // Resolves to [apiMountPoint]/
deleteUser : '/user', // Resolves to [apiMountPoint]/user/:userId
};
As seen above, the default object has a number of properties, each corresponding to a request path:
/:username
is automatically appended to the end of this route)/:userId
is automatically appended to the end of this route)The userManager module provides some middlewares.
You can get them by calling: userManager.get('middlewares');
.
This will return an object with the following middlewares:
Hooks are a mechanism to allow you hook into different parts of the application's lifecycle.
Request hooks: allow you define and register custom request middlewares. They give you the ability to modify the request any way you see fit.
Request hooks are called first before any other middleware in the middleware chain is called. This gives you great flexibility in modifying the request before handing it over to other middleware functions in the chain.
Response hooks: let you define and register custom response-modifying functions.
Response hooks are called just before the response is sent back to the client, giving you the ability to modify the response before it gets to the client.
Note: Currently, response hooks are only fired when the HTTP response status code is 200.
You can register a request or response hook for a single route, for multiple routes, or for all routes.
To register a request or response hook:
define a middleware, which is just a fancy word for a function that takes three parameters: req
, res
, next
.
If you intend to hand over processing to the next handler in the chain (recommended),
remember to call next()
from within your middleware when you are done; Or call next(error)
to pass execution to the error handler.
decide on which route (or routes) the middleware should be registered for: single-route, multi-route, global. The route(s) should correspond to a key or keys in the API endpoints configuration object.
Multiple hooks can be registered on a given route.
Register the hook:
userManager.addRequestHook(target, middlewareFn)
to register a request hookuserManager.addResponseHook(target, middlewareFn)
to register a response hookThe first parameter to addRequestHook
or addResponseHook
is a string or an array of strings that specify the target of the hook.
Possible values include:
*
: represents a global hook, which will register the hook for every pathlogin
: represents a single-route hook, which will register the hook for the specified route['login', 'signup', ...]
: represents multi-route hooks, which will register the hook for every path in the arrayRegister a request hook for every route:
userManager.addRequestHook('*', function(req, res, next) {
// Do something interesting here, with the request or the response.
req.accessTime = Date.now();
// Call next to pass control onto the next middleware function
next();
});
userManager.addResponseHook('signup', function(req, res, next) {
// You can for example set/append custom response headers:
res.append('Access-Control-Allow-Headers', '<CUSTOM_HEADER>');
});
userManager.addResponseHook(['login', 'signup'], function(req, res, next) {
// Do something tangible
res.body.data.authenticated = true;
});
userManager.addRequestHook(['login', 'signup'], ['loginFnId', 'signupFnId'])
userManager.addResponseHook('signup', [callback1, callback2, ...]);
You also have the ability to unregister a hook when the hook is no longer needed. And you can unregister hooks globally, for only some routes, or for a single route.
To unregister a request hook, call userManager.removeRequestHook(route [, callback])
.
If the optional callback
parameter is not specified, every hook registered to that route will be removed.
Otherwise, only the particular callback function specified for the hook is removed.
To unregister a response hook, call userManager.removeResponseHook(route, [callback])
;
If the optional callback
parameter is not specified, every hook registered to that route will be removed.
Otherwise, only the particular callback function specified for the hook is removed.
userManager.removeRequestHook('*');
userManager.removeResponseHook('signup');
Unregister only a request callback hook for the signup route:
UserManager.removeRequestHook('signup', fn);
fn
should be a named function registered with userManager.addRequestHook('signup', fn)
;
Anonymous functions cannot be unregistered this way.
userManager.removeRequestHook(['login', 'signup'], ['loginFnId', 'signupFnId']);
userManager.removeRequestHook(['login', 'signup']);
userManager.removeRequestHook('login', [fn1, fn2, ...]);
mariadb
)mssql
)mysql
)postgres
)sqlite
)PASSWORD_MIN_LENGTH
environment variablePASSWORD_MAX_LENGTH
environment variableDISALLOWED_PASSWORDS
environment variableThe package comes with a built-in express server that allows you run it as a stand-alone server.
To run it as a stand-alone server, do the following:
node express-user-manager/src/server
from within the parent directory containing the express-user-manager package.require('express-user-manager/src/server')();
from within a node.js
script. For example, inside an index.js
file.
Then run the file using node: node index.js
.Note: The built-in server runs using the default route/path settings. That means:
/api/users
base route (mount point).Every route below is assumed to begin (i.e., prefixed) with the base API route (or mount point).
The default base API route is /api/users
.
POST /
false
{ firstname, lastname, username, email, password, confirmPassword }
{
"data": {
"user": { id, firstname, lastname, fullname, email, username, signupDate }
}
}
GET /user/USERNAME
{
"data": {
"user": { id, firstname, lastname, fullname, email, username, signupDate }
}
}
GET /
false
firstname
(string, optional): get users matching {firstname}lastname
(string, optional): get users matching {lastname}sort
(string, optional)page
(number, optional, default = 1)limit
(number, optional, default = 20){
"data": {
"total": TOTAL_COUNT_OF_MATCHING_RESULTS,
"length": COUNT_OF_CURRENT_RESULTS_RETURNED, // determined by "page" and "limit"
"users": [
{ id, firstname, lastname, fullname, email, username, signupDate },
{ id, firstname, lastname, fullname, email, username, signupDate },
...
]
}
}
Search for users
GET /search?query=SEARCH_TERM
false
query
(string, required)sort
(string, optional)by
(string, optional)page
(number, optional, default = 1)limit
(number, optional, default = 20){
"data": {
"total": TOTAL_COUNT_OF_MATCHING_RESULTS,
"length": COUNT_OF_CURRENT_RESULTS_RETURNED, // determined by "page" and "limit"
"users": [
{ id, firstname, lastname, fullname, email, username, signupDate },
{ id, firstname, lastname, fullname, email, username, signupDate },
...
]
}
}
examples:
Search for users with james in their firstname, lastname, username, or email:
GET HOST:PORT/api/users/search?query=james
Search for users with james in their username or email:
GET HOST:PORT/api/users/search?query=james&by=username:email
Sort by firstname (asc), lastname (asc), email (desc), creationDate (asc):
GET HOST:PORT/api/users/search?query=james&sort=firstname:asc=lastname=email:desc=creationDate
Return the 3rd page of results and limit returned results to a maximum of 15 users:
GET HOST:PORT/api/users/search?query=james&page=3&limit=15
POST /login
false
{
"login": EMAIL | USERNAME,
"password": USER_PASSWORD,
}
{
"data": {
"user": { id, firstname, lastname, fullname, email, username, signupDate },
"authorization": {
"token": "Bearer TOKEN_STRING",
"expiresIn": "86400s"
}
}
}
GET /logout
false
{}
PUT /
true
{
"Authorization": "Bearer TOKEN_STRING"
}
{ id, firstname, lastname, username, email }
{
"data": {
"user": { id, firstname, lastname, fullname, email, username, signupDate }
}
}
DELETE /user/USER_ID
true
{
"Authorization": "Bearer TOKEN_STRING"
}
{
"userId": USER_ID
}
{}
See CHANGELOG