node-user-accounts-boilerplate-nahid/auth/Auth.js
"use strict";
const audit = require('../helper/audit');
const generateId = require('../helper/generateId');
const escape = require('../helper/escape');
/**
* Authenticator/passport strategy wrapper abstraction.
*
* I.e. it is the parent class of all auth classes.
*/
class Auth
{
/**
* @param {string} method
* @param {object} options common options for all auth classes; see properties
*/
constructor(method, options)
{
/**
* Authentication method name. E.g. 'email' for Email based authentication.
* This is used as an unique id for various things.
* @type string
*/
this.method = method;
/**
* This is a standard descriptor of this authentication mechanism that is publicly shared.
* Clients should use this to figure out how to use a login auth from outside.
*
* Not directly configurable.
*
* @type object
*/
this.description = {
method: this.method
};
/**
* Additional settings given to passport.authenticate.
*
* Not directly configurable.
*
* @type object
*/
this.authenticateOptions = {
failureMessage: 'login failed',
badRequestMessage: 'XXX'
};
/**
* Users collection.
*
* Note: various things all assume that a CachedCollection is being used.
*
* @type {CachedCollection}
*/
this.users = options.users || options.collection;
/**
* Default roles new registered users should assume.
*
* @type {object}
*/
this.defaultRoles = options.defaultRoles || {};
/**
* Custom fields
*
* @type {object}
*/
this.custom = options.custom || {};
//~ /**
//~ * Instance of email sender class for sending emails.
//~ *
//~ * @type {EmailSender}
//~ */
//~ this.emailSender = options.emailSender;
//~ this.defaultNotificationInterval = options.defaultNotificationInterval || -1;
//~ if (options.recaptcha)
//~ {
//~ this.description.recaptcha = true;
//~ }
/**
* Default error message to return instead of actual errors (security).
*
* @type {string}
*/
this.defaultErrorMsg = options.defaultErrorMsg || undefined;
}
/**
* Must be overridden to provide implementation of said authentication method.
* @param {ExpressApplication} app express application
* @param {string} prefix all route prefix
* @param {Passport} passport passport class
* @abstract
*/
install(app, prefix, passport)
{
throw new Error('TODO: ABSTRACT');
}
/**
* Helper method that finds an user based on a credential.
*
* A credential is something like an email address or a facebook user id.
*
* This is something that uniquely identifies an account.
*
* @param {string} value
* @return {User|false}
*/
findUser(value)
{
const users = this.users.lookup;
for (let userId in users)
{
let user = users[userId];
let credentials = (user.credentials || [])
.filter((credential) => credential.type === this.method && credential.value === value);
if (credentials.length > 0)
{
return user;
}
}
return false;
}
/**
* Helper method for SSO type logins.
*
* This method finds existing or creates new accounts basen on profile
* information returned from oauth partner.
*
* @param {string} username unique id
* @param {Profile} profile unique id
* @param {Function} done callback to call when our work is done
* @param {Request} [req] request object
*/
handleUserLoginByProfile(username, profile, done, req = undefined)
{
username = username || profile.id;
// find an account
let user = this.findUser(username);
if (user) // if found, log in found account
{
done(null, user);
}
else // if not found, make a new user and log new user in
{
user = this.createUserFromProfile(profile);
this.users.createRecord(user)
.then((user) =>
{
req && req.audit(audit.ACCOUNT_CREATE, JSON.stringify({
user,
profile
}));
done(null, user);
}, done);
}
}
/**
* Helper method that creates an User object from a Profile
*
* @param {Profile} profile
* @return {User}
*/
createUserFromProfile(profile)
{
const credential = {
type: this.method,
value: profile.id
};
let user = {
// unique id
// can't use id from profile as these might conflict across login providers
id: this.generateId(),
// login credentials
credentials: [credential],
// new user roles
roles: this.defaultRoles,
// profile bs
displayName: escape(profile.displayName),
//~ name: profile.name,
photos: profile.photos,
//~ // notification settings
//~ notifications: [],
//~ notificationInterval: -1,
//~ notificationLastSent: 0,
//~ notificationSubscriptions: {},
};
for (let field in this.custom)
{
if (this.custom[field].derive)
{
let value = this.custom[field].derive(profile);
if (value)
{
user[field] = value;
}
}
}
if (profile._json && profile._json.publicProfileUrl)
{
credential.publicUrl = profile._json.publicProfileUrl;
}
else if (profile._json && profile._json.url)
{
credential.publicUrl = profile._json.url;
}
// console.log(JSON.stringify(profile, null, 2))
return user;
}
generateId()
{
console.log(this)
let id = undefined;
while (!id || this.users.lookup[id])
{
id = generateId();
}
return id;
}
/**
* helper method that creates a profile object from a credential address
*/
async createProfileFromCredential(id, extra = {})
{
if (!id)
{
throw new Error(`id not specified: ${id}`);
}
// we don't accept spaces in id
id = id.replace(/\s+/g, '');
if (!id)
{
throw new Error(`id not specified: ${id}`);
}
let displayName = extra.displayName || (id.indexOf('@') !== -1 && id.substr(0, id.indexOf('@'))) || id;
let user = {
id,
displayName
};
return user;
}
/**
* Helper method that produces a middleware to handle successful logged in
* case.
*
* @param {boolean} [redirect=false] redirect based login is used
* @return {ExpressMiddleware}
*/
loggedIn(redirect = false)
{
if (redirect && typeof redirect !== 'string')
{
redirect = '/';
}
return function (req, res)
{
// if not req.user.id then it is not a real use
// i.e. we are forwarding error or custom payload
if (!req.user.id)
{
res.error(req.user.error, audit.LOGIN_FAILURE);
req.logout();
}
else
{
// otherwise, we make a fuss about logging in
res.audit(audit.LOGIN, `Logged in via ${this.method}`, JSON.stringify({
id: req.user.id,
displayName: req.user.displayName,
roles: req.user.roles
}));
// for redirect based login methods, we redirect back to some url
if (redirect)
{
res.redirect(redirect);
}
else
{
// otherwise we return a login success message
res.success(`Logged in via ${this.method}`, audit.LOGIN);
}
}
}.bind(this);
}
}
module.exports = Auth;